From 324cf9e095ad854ba97a4ada1d8021b2ee0f25bf Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 30 Jul 2026 12:31:39 -0400 Subject: [PATCH] :broom: --- .github/ISSUE_TEMPLATE/bug.yml | 40 +- .github/workflows/ci.yml | 8 +- .gitignore | 22 +- README.md | 39 + .../core/nms/v26_2_R1/CustomBiomeSource.java | 37 +- .../core/nms/v26_2_R1/IrisChunkGenerator.java | 56 +- .../iris/core/nms/v26_2_R1/NMSBinding.java | 71 +- ...IrisChunkGeneratorFailureContractTest.java | 21 + ...nkGeneratorMonumentLocateContractTest.java | 21 +- .../WorldgenTerrainHeightmapsTest.java | 1 + .../nativegen/NativeStructureFactoryTest.java | 2 +- ...ativeStructurePostProcessorEncaseTest.java | 24 +- ...iveStructurePostProcessorMonumentTest.java | 16 +- ...turePostProcessorScatteredFeatureTest.java | 20 +- ...ucturePostProcessorSurfaceTerrainTest.java | 179 +- ...eStructurePostProcessorVegetationTest.java | 30 +- .../src/main/java/art/arcane/iris/Iris.java | 613 +----- .../iris/core/BukkitWorldReconciler.java | 110 + .../iris/core/IrisWorldGeneratorResolver.java | 173 ++ .../iris/core/PendingWorldDeleteQueue.java | 228 ++ .../iris/core/SettingsHotloadWatch.java | 98 + .../iris/core/commands/CommandDeveloper.java | 58 +- .../iris/core/commands/CommandIris.java | 15 +- .../iris/core/commands/CommandObject.java | 260 ++- .../iris/core/commands/CommandStudio.java | 64 +- .../iris/core/commands/CommandWhat.java | 147 +- .../iris/core/edit/BukkitBlockEditor.java | 15 +- .../iris/core/gui/BukkitVisionOverlay.java | 124 +- .../runtime/BukkitEnginePlatformHooks.java | 3 +- .../arcane/iris/core/service/FellingRun.java | 45 + .../iris/core/service/IrisEngineSVC.java | 5 + .../core/service/RoutedBlockBreakEvent.java | 22 + .../iris/core/service/TreeFellerModel.java | 81 + .../iris/core/service/TreeFellerSVC.java | 848 +------- .../iris/core/service/TreeFellingRunner.java | 590 +++++ .../iris/core/service/TreeProvenance.java | 151 ++ .../art/arcane/iris/core/service/WandSVC.java | 22 +- .../core/service/terrain/IrisColumnWalk.java | 10 +- .../arcane/iris/core/wand/WandSelection.java | 4 + .../service/TreeFellerEventOrderTest.java | 2 +- .../arcane/iris/client/IrisTileAssembler.java | 17 +- .../art/arcane/iris/client/IrisTileCodec.java | 4 + adapters/fabric/build.gradle | 5 +- adapters/fabric/settings.gradle | 47 +- .../fabric/mixin/PackRepositoryMixin.java | 8 + .../fabric/src/main/resources/fabric.mod.json | 6 +- adapters/forge/settings.gradle | 47 +- .../nativegen/NativeStructureFactory.java | 4 +- .../NativeStructureFoundationBuilder.java | 215 ++ .../NativeStructurePostProcessor.java | 1587 +------------- .../nativegen/NativeStructureReflection.java | 148 ++ .../NativeStructureSurfaceFitter.java | 279 +++ .../NativeStructureTerrainIntegrator.java | 508 +++++ .../NativeStructureVegetationClearer.java | 174 ++ .../NativeStructureVerticalPlacer.java | 365 ++++ .../nativegen}/WorldgenTerrainHeightmaps.java | 14 +- .../iris/modded/IrisModdedBiomeSource.java | 27 +- .../iris/modded/IrisModdedChunkGenerator.java | 645 +----- .../iris/modded/ModdedBlockResolution.java | 16 +- .../arcane/iris/modded/ModdedBlockState.java | 12 + .../iris/modded/ModdedDimensionMetadata.java | 98 + .../modded/ModdedDimensionRegistryStore.java | 44 +- .../iris/modded/ModdedEngineBinding.java | 77 + .../iris/modded/ModdedEntitySpawner.java | 2 +- .../iris/modded/ModdedForcedDatapack.java | 23 +- .../art/arcane/iris/modded/ModdedGenPool.java | 87 + .../art/arcane/iris/modded/ModdedIrisLog.java | 3 +- .../arcane/iris/modded/ModdedLootApplier.java | 18 +- .../modded/ModdedNativeStructureStage.java | 439 ++++ .../arcane/iris/modded/ModdedPlatform.java | 5 +- .../iris/modded/ModdedSpawnTableMerger.java | 119 ++ .../art/arcane/iris/modded/ModdedStartup.java | 3 + .../iris/modded/ModdedStateRotator.java | 17 +- .../arcane/iris/modded/ModdedTileReader.java | 2 +- .../arcane/iris/modded/ModdedWorldCheck.java | 1060 +-------- .../iris/modded/ModdedWorldEngines.java | 2 +- .../modded/WorldCheckDimensionContract.java | 114 + .../iris/modded/WorldCheckMaterials.java | 130 ++ .../iris/modded/WorldCheckPredicates.java | 130 ++ .../iris/modded/WorldCheckStructureAudit.java | 783 +++++++ .../arcane/iris/modded/api/IrisModdedAPI.java | 112 + .../iris/modded/api/ModdedBlockData.java | 24 + .../api/ModdedBlockPlacementContext.java | 19 + .../api/ModdedCustomContentRegistry.java | 80 +- .../iris/modded/api/ModdedDataProvider.java | 63 + .../iris/modded/api/ModdedDataType.java | 8 + .../modded/command/IrisModdedCommands.java | 1119 +--------- .../command/ModdedCommandSuggestions.java | 191 ++ .../modded/command/ModdedCommandTree.java | 344 +++ .../modded/command/ModdedDustRevealer.java | 10 +- .../modded/command/ModdedEditCommands.java | 133 ++ .../modded/command/ModdedLocateCommands.java | 535 +++++ .../modded/command/ModdedObjectCommands.java | 3 +- .../modded/command/ModdedPregenCommands.java | 101 + .../modded/command/ModdedStudioCommands.java | 3 +- .../modded/command/ModdedWorldCommands.java | 10 +- .../ModdedJigsawStructureCapture.java | 620 ------ .../ModdedStructureImportService.java | 426 ---- .../ModdedStructureTemplateCapture.java | 339 --- .../IrisModdedChunkGeneratorSpawnTest.java | 4 +- .../modded/IrisModdedStructureParityTest.java | 30 +- .../modded/ModdedDimensionTypeParityTest.java | 10 +- .../iris/modded/ModdedWorldCheckTest.java | 85 +- .../NativeStructureFailureContractTest.java | 40 +- .../IrisModdedStructureCommandTest.java | 17 +- .../ModdedJigsawStructureCaptureTest.java | 69 - .../ModdedStructureImportServiceTest.java | 86 - .../ModdedStructureTemplateCaptureTest.java | 184 -- adapters/neoforge/gradle.properties | 5 + adapters/neoforge/settings.gradle | 47 +- build.gradle | 5 +- core/purity-allowlist.txt | 14 +- .../art/arcane/iris/core/IrisSettings.java | 83 +- .../core/datapack/DatapackIngestService.java | 95 +- .../arcane/iris/core/gui/PregeneratorJob.java | 13 +- .../iris/core/gui/components/TileRender.java | 31 - .../iris/core/loader/ImageResourceLoader.java | 63 +- .../art/arcane/iris/core/loader/IrisData.java | 56 +- .../loader/MatterObjectResourceLoader.java | 72 +- .../core/loader/ObjectResourceLoader.java | 31 +- .../iris/core/loader/ResourceLoader.java | 193 +- .../iris/core/nms/container/AutoClosing.java | 39 - .../iris/core/pack/ContentKeyValidator.java | 157 ++ .../core/pack/PackDimensionValidator.java | 193 ++ .../arcane/iris/core/pack/PackDownloader.java | 3 +- .../iris/core/pack/PackJsonFieldChecks.java | 101 + .../iris/core/pack/PackLootValidator.java | 240 +++ .../pack/PackNativeStructureValidator.java | 341 +++ .../core/pack/PackObjectSurfaceValidator.java | 227 ++ .../iris/core/pack/PackSpawnValidator.java | 343 +++ .../pack/PackStructurePlacementValidator.java | 389 ++++ .../iris/core/pack/PackValidationIo.java | 127 ++ .../arcane/iris/core/pack/PackValidator.java | 1893 +---------------- .../core/pregenerator/IrisPregenerator.java | 49 +- .../PregenMantleBackpressure.java | 37 + .../pregenerator/cache/PregenCacheImpl.java | 7 +- .../methods/AsyncPregenMethod.java | 278 +-- .../methods/DummyPregenMethod.java | 65 - .../methods/MedievalPregenMethod.java | 103 +- .../iris/core/project/IrisCodeWorkspace.java | 298 +++ .../core/project/IrisPackageCompiler.java | 252 +++ .../arcane/iris/core/project/IrisProject.java | 909 +------- .../iris/core/project/IrisProjectCleaner.java | 147 ++ .../core/project/IrisProjectCompiler.java | 152 ++ .../project/StudioOpenProgressReporter.java | 250 +++ .../core/runtime/StudioOpenCoordinator.java | 3 +- .../iris/core/service/ExternalDataSVC.java | 27 +- .../iris/core/service/GlobalCacheSVC.java | 11 +- .../iris/core/service/PreservationSVC.java | 4 + .../arcane/iris/core/service/StudioSVC.java | 10 +- .../studio/SimpleStructureStudioCell.java | 315 --- .../studio/SimpleStructureStudioCompiler.java | 401 ---- .../SimpleStructureStudioDirection.java | 74 - .../studio/SimpleStructureStudioDraft.java | 140 -- .../studio/SimpleStructureStudioLayout.java | 68 - .../SimpleStructureStudioPublishConfig.java | 66 - .../SimpleStructureStudioRepository.java | 113 - .../SimpleStructureStudioRotationPolicy.java | 52 - .../studio/SimpleStructureStudioSession.java | 233 -- .../studio/SimpleStructureStudioTopology.java | 54 - .../studio/SimpleStructureStudioVariant.java | 43 - .../SimpleStructureStudioVariantKey.java | 42 - .../iris/core/tools/IrisPackBenchmarking.java | 11 +- .../iris/engine/EngineBackgroundTasks.java | 158 ++ .../arcane/iris/engine/EngineDataStore.java | 138 ++ .../arcane/iris/engine/EngineHotloader.java | 194 ++ .../iris/engine/EngineMetricsReport.java | 112 + .../art/arcane/iris/engine/EngineRuntime.java | 63 + .../iris/engine/EngineRuntimeBuilder.java | 312 +++ .../iris/engine/EngineShutdownSequence.java | 291 +++ .../iris/engine/EngineTickRegistry.java | 75 + .../art/arcane/iris/engine/IrisComplex.java | 289 ++- .../art/arcane/iris/engine/IrisEngine.java | 1100 +--------- .../arcane/iris/engine/IrisWorldManager.java | 1273 +---------- .../iris/engine/MarkerSpawnScanner.java | 224 ++ .../iris/engine/UpperDimensionContext.java | 2 +- .../iris/engine/WorldBlockDropRouter.java | 134 ++ .../iris/engine/WorldChunkMaintenance.java | 405 ++++ .../iris/engine/WorldEntitySpawner.java | 558 +++++ .../iris/engine/WorldTeleportWarmup.java | 104 + .../engine/actuator/IrisBiomeActuator.java | 69 +- .../actuator/IrisTerrainNormalActuator.java | 122 -- .../iris/engine/data/cache/AtomicCache.java | 76 +- .../iris/engine/data/cache/Multicache.java | 26 - .../engine/data/chunk/LinkedTerrainChunk.java | 15 + .../iris/engine/data/io/Deserializer.java | 60 - .../engine/data/io/ExceptionBiFunction.java | 25 - .../engine/data/io/ExceptionTriConsumer.java | 25 - .../iris/engine/data/io/MaxDepthIO.java | 31 - .../data/io/MaxDepthReachedException.java | 30 - .../iris/engine/data/io/Serializer.java | 44 - .../engine/data/io/StringDeserializer.java | 55 - .../iris/engine/data/io/StringSerializer.java | 53 - .../iris/engine/decorator/DecoratorCore.java | 16 +- .../framework/EngineAssignedBiModifier.java | 34 - .../framework/EngineAssignedModifier.java | 14 +- .../engine/framework/EngineBiModifier.java | 25 - .../iris/engine/framework/EngineData.java | 58 - .../engine/framework/PregeneratedData.java | 64 - .../iris/engine/framework/ResultLocator.java | 112 - .../iris/engine/mantle/EngineMantle.java | 17 - .../iris/engine/mantle/MantleWriter.java | 77 +- .../iris/engine/mantle/MatterGenerator.java | 358 ++-- .../mantle/components/CaveCarveScratch.java | 71 + .../components/CaveFieldModuleState.java | 58 + .../components/CaveWaterSupportPlan.java | 120 ++ .../components/GoldenDebugObjectPlacer.java | 159 ++ .../mantle/components/IrisCaveCarver3D.java | 455 ++-- .../components/MantleCarvingComponent.java | 28 +- .../components/MantleObjectComponent.java | 148 +- .../engine/modifier/IrisCarveModifier.java | 12 +- .../IrisFloatingChildBiomeModifier.java | 2 +- .../engine/modifier/IrisPostModifier.java | 181 +- .../art/arcane/iris/engine/object/IRare.java | 4 +- .../arcane/iris/engine/object/IrisBiome.java | 581 +---- .../engine/object/IrisBiomeColorRenderer.java | 93 + .../engine/object/IrisBiomeDerivatives.java | 152 ++ .../iris/engine/object/IrisBiomeGenLinks.java | 137 ++ .../engine/object/IrisBiomeGeneratorLink.java | 8 +- .../object/IrisBiomeLayerGenerator.java | 356 ++++ .../iris/engine/object/IrisBiomeOres.java | 119 ++ .../engine/object/IrisBiomePaletteLayer.java | 54 +- .../iris/engine/object/IrisCaveShape.java | 85 - .../iris/engine/object/IrisDecorator.java | 97 +- .../engine/object/IrisDepositGenerator.java | 4 +- .../arcane/iris/engine/object/IrisEffect.java | 5 +- .../iris/engine/object/IrisFontStyle.java | 33 - .../iris/engine/object/IrisGenerator.java | 42 - .../arcane/iris/engine/object/IrisImage.java | 3 +- .../iris/engine/object/IrisInterpolator.java | 12 +- .../engine/object/IrisInterpolator3D.java | 58 - .../arcane/iris/engine/object/IrisObject.java | 1649 +------------- .../iris/engine/object/IrisObjectIO.java | 296 +++ .../iris/engine/object/IrisObjectLimit.java | 4 +- .../object/IrisObjectPlacementRunner.java | 1118 ++++++++++ .../engine/object/IrisObjectRotation.java | 2 +- .../iris/engine/object/IrisObjectShaping.java | 279 +++ .../engine/object/IrisObjectTransforms.java | 269 +++ .../engine/object/IrisObjectTranslate.java | 16 +- .../iris/engine/object/IrisPosition2D.java | 40 - .../engine/object/IrisProceduralBlocks.java | 47 +- .../iris/engine/object/IrisRareObject.java | 49 - .../arcane/iris/engine/object/IrisSeed.java | 47 - .../object/IrisShapedGeneratorStyle.java | 8 +- .../arcane/iris/engine/object/IrisWorm.java | 116 - .../arcane/iris/engine/object/TileData.java | 2 +- .../RegistryListBiomeDownfallType.java | 33 - .../functions/ResourceLoadersFunction.java | 28 - .../object/matter/IrisMatterPlacement.java | 95 - .../matter/IrisMatterPlacementLocation.java | 23 - .../object/matter/IrisMatterTranslate.java | 56 - .../platform/bukkit/BukkitBlockState.java | 12 + .../iris/platform/bukkit/BukkitPlatform.java | 11 +- .../iris/util/common/board/BoardEntry.java | 21 - .../iris/util/common/data/BiomeMap.java | 38 - .../iris/util/common/data/VectorMap.java | 112 +- .../util/common/data/palette/BitStorage.java | 172 -- .../CrudeIncrementalIntIdentityHashBiMap.java | 189 -- .../common/data/palette/GlobalPalette.java | 70 - .../common/data/palette/HashMapPalette.java | 74 - .../iris/util/common/data/palette/IdMap.java | 25 - .../util/common/data/palette/IdMapper.java | 87 - .../common/data/palette/LinearPalette.java | 83 - .../iris/util/common/data/palette/Mth.java | 710 ------- .../util/common/data/palette/Palette.java | 33 - .../common/data/palette/PaletteAccess.java | 31 - .../common/data/palette/PaletteResize.java | 23 - .../util/common/data/palette/PaletteType.java | 51 - .../data/palette/PalettedContainer.java | 156 -- .../util/common/data/palette/QuartPos.java | 43 - .../specialhandlers/DummyHandler.java | 7 - .../common/inventorygui/ElementEvent.java | 18 - .../common/inventorygui/UIVoidDecorator.java | 30 - .../iris/util/common/math/AxisAlignedBB.java | 8 - .../arcane/iris/util/common/math/RNGV2.java | 167 -- .../util/common/parallel/BurstedHunk.java | 24 - .../util/common/parallel/NOOPGridLock.java | 9 - .../util/common/parallel/StreamUtils.java | 29 - .../util/common/parallel/SyncExecutor.java | 20 - .../iris/util/common/plugin/Command.java | 33 - .../iris/util/common/plugin/CommandDummy.java | 148 -- .../iris/util/common/plugin/Control.java | 31 - .../iris/util/common/plugin/Controller.java | 78 - .../iris/util/common/plugin/ICommand.java | 70 - .../iris/util/common/plugin/IController.java | 42 - .../iris/util/common/plugin/Instance.java | 31 - .../util/common/plugin/MortarCommand.java | 197 -- .../util/common/plugin/MortarPermission.java | 99 - .../iris/util/common/plugin/Permission.java | 31 - .../util/common/plugin/PluginRegistry.java | 63 - .../common/plugin/PluginRegistryGroup.java | 59 - .../util/common/plugin/RouterCommand.java | 64 - .../util/common/plugin/VirtualCommand.java | 188 -- .../iris/util/common/plugin/VolmitPlugin.java | 394 ---- .../iris/util/common/plugin/VolmitSender.java | 11 - .../iris/util/common/reflect/OldEnum.java | 99 - .../arcane/iris/util/common/scheduling/J.java | 89 +- .../iris/util/common/scheduling/jobs/Job.java | 4 +- .../scheduling/jobs/ParallelRadiusJob.java | 86 - .../util/project/context/ChunkContext.java | 6 +- .../arcane/iris/util/project/hunk/Hunk.java | 3 +- .../project/hunk/storage/PaletteOrHunk.java | 37 - .../hunk/view/TerrainChunkBiomeHunkView.java | 17 + .../interpolation/Interpolation3D.java | 322 +++ .../interpolation/IrisInterpolation.java | 759 +------ .../project/interpolation/NoiseBounds.java} | 8 +- .../interpolation/NoiseBoundsProvider.java} | 6 +- .../NoiseBoundsSampleCache2D.java | 201 ++ .../interpolation/NoiseSampleCache2D.java | 149 ++ .../arcane/iris/util/project/noise/CNG.java | 7 - .../iris/util/project/noise/CachedNoise.java | 42 - .../util/project/noise/CachedNoiseMap.java | 78 - .../project/noise/RarityCellGenerator.java | 71 - .../util/project/profile/MsptTimings.java | 84 - .../util/project/stream/ProceduralStream.java | 4 +- .../interpolation/TriHermiteStream.java | 149 -- .../stream/utility/CachedStream2D.java | 2 +- .../PackValidatorCustomBiomeSpawnTest.java | 46 +- ...kValidatorImportedStructurePolicyTest.java | 6 +- .../iris/core/pack/PackValidatorLootTest.java | 16 +- ...PackValidatorRemovedWorldgenFieldTest.java | 2 +- .../pack/PackValidatorSpawnerEntityTest.java | 2 +- .../pack/PackValidatorStructureGraphTest.java | 52 +- .../PackValidatorStructureTransformTest.java | 4 +- .../pack/PackValidatorSurfaceSupportTest.java | 6 +- .../IrisProjectEntityDependencyTest.java | 4 +- .../core/project/SchemaBuilderParityTest.java | 3 +- .../SimpleStructureStudioCompilerTest.java | 251 --- .../SimpleStructureStudioModelTest.java | 132 -- .../SimpleStructureStudioRepositoryTest.java | 75 - .../SimpleStructureStudioSessionTest.java | 140 -- .../IrisComplexGridBoundsCacheTest.java | 9 +- .../engine/IrisEngineDataPersistenceTest.java | 6 +- .../engine/IrisEngineLifecycleGateTest.java | 4 +- .../IrisEnginePlatformHookIsolationTest.java | 7 + .../engine/IrisWorldManagerMarkerTest.java | 20 +- .../IrisCaveCarver3DNearParityTest.java | 6 +- .../IrisMathNoiseHotPathParityTest.java | 6 +- .../IrisObjectStructurePieceAirTest.java | 14 +- .../context/ChunkContextPrefillPlanTest.java | 35 +- docs/api/README.md | 13 +- docs/api/modded.md | 423 ++++ docs/mc-version-bump.md | 168 ++ docs/release-checklist.md | 87 + docs/release-readiness-checklist.md | 490 +++++ gradle/volmlib-resolution.settings.gradle | 98 + listing.json | 10 - logs/latest.log | 0 packignore.ignore | 3 - .../art/arcane/iris/probe/StubPlatform.java | 2 +- .../main/resources/classload-allowlist.tsv | 4 - settings.gradle | 72 +- .../java/art/arcane/iris/spi/IrisLogging.java | 47 +- .../art/arcane/iris/spi/IrisPlatform.java | 92 +- .../art/arcane/iris/spi/IrisPlatforms.java | 24 + .../art/arcane/iris/spi/IrisServices.java | 34 + .../java/art/arcane/iris/spi/LogLevel.java | 13 +- .../art/arcane/iris/spi/PlatformBiome.java | 14 + .../arcane/iris/spi/PlatformBiomeWriter.java | 16 + .../iris/spi/PlatformBlockProperty.java | 16 + .../arcane/iris/spi/PlatformBlockState.java | 115 + .../arcane/iris/spi/PlatformEntityType.java | 19 + .../art/arcane/iris/spi/PlatformItem.java | 15 + .../arcane/iris/spi/PlatformNumericRange.java | 13 + .../arcane/iris/spi/PlatformRegistries.java | 66 + .../arcane/iris/spi/PlatformScheduler.java | 30 + .../iris/spi/PlatformStructureHooks.java | 50 + .../art/arcane/iris/spi/PlatformWorld.java | 49 + .../arcane/iris/spi/protocol/IrisMessage.java | 75 + .../iris/spi/protocol/IrisMessageCodec.java | 23 + .../iris/spi/protocol/IrisProtocol.java | 22 + .../iris/spi/protocol/IrisWireReader.java | 50 + .../iris/spi/protocol/IrisWireWriter.java | 38 + .../iris/spi/protocol/ProtocolException.java | 12 + 374 files changed, 22705 insertions(+), 25650 deletions(-) create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java create mode 100644 adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java create mode 100644 adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java rename adapters/{bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1 => minecraft-common/src/main/java/art/arcane/iris/nativegen}/WorldgenTerrainHeightmaps.java (90%) create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java delete mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCapture.java delete mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureImportService.java delete mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCapture.java delete mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCaptureTest.java delete mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureImportServiceTest.java delete mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCaptureTest.java create mode 100644 adapters/neoforge/gradle.properties delete mode 100644 core/src/main/java/art/arcane/iris/core/gui/components/TileRender.java delete mode 100644 core/src/main/java/art/arcane/iris/core/nms/container/AutoClosing.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java delete mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/methods/DummyPregenMethod.java create mode 100644 core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java create mode 100644 core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java create mode 100644 core/src/main/java/art/arcane/iris/core/project/IrisProjectCleaner.java create mode 100644 core/src/main/java/art/arcane/iris/core/project/IrisProjectCompiler.java create mode 100644 core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCell.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompiler.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDirection.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDraft.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioLayout.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioPublishConfig.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepository.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRotationPolicy.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSession.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioTopology.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariant.java delete mode 100644 core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariantKey.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineDataStore.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineHotloader.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineRuntime.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java create mode 100644 core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java create mode 100644 core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java create mode 100644 core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java create mode 100644 core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java create mode 100644 core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java create mode 100644 core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/cache/Multicache.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/ExceptionBiFunction.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/ExceptionTriConsumer.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthIO.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthReachedException.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedBiModifier.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/framework/EngineBiModifier.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/framework/EngineData.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/framework/PregeneratedData.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/framework/ResultLocator.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/components/CaveWaterSupportPlan.java create mode 100644 core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisFontStyle.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisPosition2D.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisSeed.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisWorm.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ResourceLoadersFunction.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacementLocation.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterTranslate.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/board/BoardEntry.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/BiomeMap.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/BitStorage.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/CrudeIncrementalIntIdentityHashBiMap.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/GlobalPalette.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/HashMapPalette.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/IdMap.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/IdMapper.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/LinearPalette.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/Mth.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/Palette.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteAccess.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteResize.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteType.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/PalettedContainer.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/data/palette/QuartPos.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/DummyHandler.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/inventorygui/ElementEvent.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/inventorygui/UIVoidDecorator.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/math/RNGV2.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/parallel/BurstedHunk.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/parallel/NOOPGridLock.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/parallel/StreamUtils.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/parallel/SyncExecutor.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/Command.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/Control.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/Controller.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/ICommand.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/IController.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/Instance.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/MortarCommand.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/MortarPermission.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/Permission.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistry.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistryGroup.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/RouterCommand.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/reflect/OldEnum.java delete mode 100644 core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelRadiusJob.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/hunk/storage/PaletteOrHunk.java create mode 100644 core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java rename core/src/main/java/art/arcane/iris/{engine/mantle/MantleSized.java => util/project/interpolation/NoiseBounds.java} (80%) rename core/src/main/java/art/arcane/iris/util/{common/data/palette/CountConsumer.java => project/interpolation/NoiseBoundsProvider.java} (85%) create mode 100644 core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java create mode 100644 core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/noise/CachedNoise.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/noise/CachedNoiseMap.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/noise/RarityCellGenerator.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java delete mode 100644 core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriHermiteStream.java delete mode 100644 core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompilerTest.java delete mode 100644 core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioModelTest.java delete mode 100644 core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepositoryTest.java delete mode 100644 core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSessionTest.java create mode 100644 docs/api/modded.md create mode 100644 docs/mc-version-bump.md create mode 100644 docs/release-checklist.md create mode 100644 docs/release-readiness-checklist.md create mode 100644 gradle/volmlib-resolution.settings.gradle delete mode 100644 listing.json delete mode 100644 logs/latest.log delete mode 100644 packignore.ignore diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4de1b3040..9052869c2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -27,22 +27,40 @@ body: placeholder: The code to place a is missing b and c... validations: required: false + - type: dropdown + id: platform + attributes: + label: Platform + description: Which server platform is Iris running on? Pick the loader or server software, not the launcher. + options: + - Paper + - Purpur + - Leaf + - Canvas + - Folia + - Spigot / CraftBukkit + - Fabric + - Forge + - NeoForge + - Other (describe under Problem) + validations: + required: true - type: dropdown id: mcversion attributes: label: Minecraft Version - description: What version of Minecraft is the server on? + description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported. options: - - 1.14.X - - 1.15.X - - 1.16.X - - 1.17 - - 1.17.1 - - 1.18 - - 1.19 - - 1.20 - - 1.21 - - 1.22 + - '26.2' + - Other (unsupported) + validations: + required: true + - type: input + id: loaderversion + attributes: + label: Platform / Loader Version + description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta". + placeholder: DO NOT SAY "LATEST" validations: required: true - type: input diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51a820a65..08c285824 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,13 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - name: Run verification gates - run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace - - name: Run modded shared tests + run: ./gradlew :core:check :adapters:bukkit:plugin:test :adapters:bukkit:nms:v26_2_R1:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace + - name: Run modded shared tests (Fabric) run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace + - name: Run modded shared tests (Forge) + run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace + - name: Run modded shared tests (NeoForge) + run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace - name: Test modded artifact verifier run: ./gradlew -p buildSrc test --console=plain --stacktrace - name: Verify modded artifacts diff --git a/.gitignore b/.gitignore index 30ca5cd36..42187b288 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,16 @@ TreeGenStuff/ dist/ core/plugins/ adapters/bukkit/plugin/plugins/ -adapters/*/run/ -adapters/**/logs/*.gz + +# Dev-run artifacts. `run/` is a loom/ForgeGradle/ModDevGradle working directory and `logs/` is +# server output; neither is ever source. No source directory in this repo is named run or logs. +run/ +logs/ +*.log +*.log.gz +crash-reports/ +hs_err_pid*.log +replay_pid*.log .codegraph/ @@ -40,12 +48,14 @@ local.properties credentials.json service-account*.json -docs/* -!docs/api/ - -CROSSPLATFORM_PLAN.md +# docs/ is hand-written and tracked (maintainer checklists + docs/api/). Nothing under it is +# generated, so there is nothing to ignore here. .qa/ .repro/ +# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source. +.apiwt/ +.papiwt/ + __pycache__/ diff --git a/README.md b/README.md index 211cc3976..810d2350b 100644 --- a/README.md +++ b/README.md @@ -192,10 +192,49 @@ Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `build developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by `./gradlew :spi:jar`. +`./gradlew buildAll` is a different task: it builds every platform and copies the jars into a +consumer dropin tree for a local test server. It defaults to `build/consumers/` inside the repo; +override with `-Plocation=/path/to/consumers`. + If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the Discord asking for free copies or a compile tutorial. +## Adapters / modded development + +`core/` and `spi/` are pure JVM. `adapters/bukkit/` is part of the root Gradle build; the three +modded adapters (`adapters/fabric`, `adapters/forge`, `adapters/neoforge`) are standalone builds +with their own `settings.gradle`, which is what keeps Loom, ForgeGradle, and ModDevGradle off one +plugin classpath. Drive them with `-p`: + +``` +./gradlew -p adapters/fabric runServer # or runClient +./gradlew -p adapters/forge runServer +./gradlew -p adapters/neoforge runServer +./gradlew -p adapters/fabric test # shared adapters/modded-common test suite +``` + +Each `runServer` accepts determinism and world-integrity flags, forwarded to the game as system +properties: + +| Flag | System property | Purpose | +|---|---|---| +| `-PirisParity=` | `iris.parity` | Run the cross-platform parity harness for a pack | +| `-PirisParityGolden=` | `iris.parity.golden` | Compare against a captured golden-hash file | +| `-PirisParityDeep=true` | `iris.parity.deep` | Deep (per-block) parity instead of hash-only | +| `-PirisWorldCheck=` | `iris.worldcheck` | Post-generation world integrity check | + +Fabric additionally takes `-PirisClientRunDir=` to relocate the `runClient` working directory. +Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common` +(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type +screens); every adapter adds those source directories, so one edit reaches all three loaders. + +For IDE import you can surface the three adapter builds in the root composite with +`-PincludeModdedAdapters=true`. It is off by default: each adapter includes the root build back to +substitute `art.arcane:core` and `art.arcane:spi`, so including them from the root closes a +composite cycle. The build and release paths do not need it. + ## Maintainer docs - [Minecraft version bump checklist](docs/mc-version-bump.md) - [Release checklist](docs/release-checklist.md) +- [Release readiness checklist](docs/release-readiness-checklist.md) diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java index 1fbe4a574..c863df632 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java @@ -106,11 +106,26 @@ public class CustomBiomeSource extends BiomeSource { return o; } - return invokeFor(type, source); + o = invokeFor(type, source); + + if (o != null) { + return o; + } + + throw new IllegalStateException("Iris cannot resolve a " + type.getName() + + " from " + source.getClass().getName() + " on this server version"); } private static Object fieldFor(Class returns, Object in) { - return fieldForClass(returns, in.getClass(), in); + for (Class sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) { + Object o = fieldForClass(returns, sourceType, in); + + if (o != null) { + return o; + } + } + + return null; } private static Object invokeFor(Class returns, Object in) { @@ -120,8 +135,9 @@ public class CustomBiomeSource extends BiomeSource { try { IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); return i.invoke(in); - } catch (Throwable e) { - e.printStackTrace(); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "." + + i.getName() + "() for " + returns.getName(), e); } } } @@ -137,8 +153,9 @@ public class CustomBiomeSource extends BiomeSource { try { IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); return (T) i.get(in); - } catch (IllegalAccessException e) { - e.printStackTrace(); + } catch (IllegalAccessException | RuntimeException e) { + throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "." + + i.getName() + " for " + returnType.getName(), e); } } } @@ -241,7 +258,13 @@ public class CustomBiomeSource extends BiomeSource { } private RegistryAccess registry() { - return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + + if (access == null) { + throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version"); + } + + return access; } @Override 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 0b8764770..ace3cad0f 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 @@ -15,6 +15,11 @@ import art.arcane.iris.nativegen.NativeStructureStartInjector; import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope; import art.arcane.iris.nativegen.NativeStructureLocateResults; import art.arcane.iris.nativegen.NativeStructurePostProcessor; +import art.arcane.iris.nativegen.NativeStructureSurfaceFitter; +import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator; +import art.arcane.iris.nativegen.NativeStructureVegetationClearer; +import art.arcane.iris.nativegen.NativeStructureVerticalPlacer; +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.IrisCustomData; import art.arcane.iris.util.common.reflect.WrappedField; @@ -184,7 +189,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } String key = id.toString(); IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, - key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step())); + key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step())); if (!decision.generate()) { continue; } @@ -290,7 +295,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { throw NativeStructureGenerationException.failure( "resolution", null, chunkPos.x(), chunkPos.z()); } - boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step()); + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); IrisNativeStructureDecision decision; try { decision = NativeStructureGenerationPolicy.resolve(engine, @@ -304,7 +309,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { continue; } try { - NativeStructurePostProcessor.applyVerticalPlacement( + NativeStructureVerticalPlacer.applyVerticalPlacement( start, structureId, decision.yShift(), @@ -317,7 +322,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight()); StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( start, structure, start.getReferences(), templateManager, - NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain())); + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain())); access.setStartForStructure(structure, wrapped); } catch (Throwable error) { throw NativeStructureGenerationException.failure( @@ -448,8 +453,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator { List placementGroups = new ArrayList<>(); List heightmapStarts = new ArrayList<>(); List nativeStarts = new ArrayList<>(); - List vegetationTargets = new ArrayList<>(); - List terrainTargets = new ArrayList<>(); + List vegetationTargets = new ArrayList<>(); + List terrainTargets = new ArrayList<>(); for (int step = 0; step < steps; step++) { int index = 0; for (Structure structure : byStep.get(step)) { @@ -461,7 +466,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } try { IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(engine, - structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step())); + structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step())); List starts = structureManager.startsForStructure(sectionPos, structure); List resolvedPlacements = new ArrayList<>(starts.size()); for (StructureStart start : starts) { @@ -474,17 +479,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } resolvedPlacements.add(new NativePlacement(start, decision)); heightmapStarts.add(start); - terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget( + terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget( structureId, start, - NativeStructurePostProcessor.resolveNativeTerrain( + NativeStructureTerrainIntegrator.resolveNativeTerrain( start, decision.terrain()))); if (plan == null || !plan.placement().isUnderground()) { nativeStarts.add(start); } - boolean clearEntireFootprint = NativeStructurePostProcessor + boolean clearEntireFootprint = NativeStructureVegetationClearer .shouldClearEntireVegetationFootprint( structure.step(), decision.clearVegetation()); - vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget( + vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget( start, clearEntireFootprint)); } if (!resolvedPlacements.isEmpty()) { @@ -507,7 +512,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { chunkPos.x(), chunkPos.z(), error); } try { - NativeStructurePostProcessor.prepareSurfaceStructures( + NativeStructureSurfaceFitter.prepareSurfaceStructures( world, area, nativeStarts, (x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight()); } catch (Throwable error) { @@ -516,7 +521,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { chunkPos.x(), chunkPos.z(), error); } try { - NativeStructurePostProcessor.clearIntersectingVegetation( + NativeStructureVegetationClearer.clearIntersectingVegetation( world, chunk, area, vegetationTargets); } catch (Throwable error) { throw NativeStructureGenerationException.failure( @@ -741,27 +746,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } static { - Field biomeSource = null; + List biomeSources = new ArrayList<>(1); for (Field field : ChunkGenerator.class.getDeclaredFields()) { if (!field.getType().equals(BiomeSource.class)) continue; - biomeSource = field; - break; + biomeSources.add(field); } - if (biomeSource == null) - throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!"); + if (biomeSources.size() != 1) + throw new IllegalStateException("Expected exactly one BiomeSource field in ChunkGenerator, found " + + biomeSources.size() + " " + biomeSources.stream().map(Field::getName).toList()); + Field biomeSource = biomeSources.getFirst(); - Method setHeight = null; + List setHeights = new ArrayList<>(1); for (Method method : Heightmap.class.getDeclaredMethods()) { Class[] types = method.getParameterTypes(); - if (types.length != 3 || !Arrays.equals(types, new Class[]{int.class, int.class, int.class}) + if (!method.getName().equals("setHeight") + || !Arrays.equals(types, new Class[]{int.class, int.class, int.class}) || !method.getReturnType().equals(void.class)) continue; - setHeight = method; - break; + setHeights.add(method); } - if (setHeight == null) - throw new RuntimeException("Could not find setHeight method in Heightmap!"); + if (setHeights.size() != 1) + throw new IllegalStateException("Expected exactly one Heightmap.setHeight(int,int,int) method, found " + + setHeights.size()); + Method setHeight = setHeights.getFirst(); BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName()); SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes()); 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 d269ca4b4..ab372d7d2 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 @@ -171,7 +171,14 @@ public class NMSBinding implements INMSBinding { return o; } - return invokeFor(type, source); + o = invokeFor(type, source); + + if (o != null) { + return o; + } + + throw new IllegalStateException("Iris cannot resolve a " + type.getName() + + " from " + source.getClass().getName() + " on this server version"); } private static Object invokeFor(Class returns, Object in) { @@ -181,8 +188,9 @@ public class NMSBinding implements INMSBinding { try { IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); return i.invoke(in); - } catch (Throwable e) { - e.printStackTrace(); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "." + + i.getName() + "() for " + returns.getName(), e); } } } @@ -191,7 +199,15 @@ public class NMSBinding implements INMSBinding { } private static Object fieldFor(Class returns, Object in) { - return fieldForClass(returns, in.getClass(), in); + for (Class sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) { + Object o = fieldForClass(returns, sourceType, in); + + if (o != null) { + return o; + } + } + + return null; } @SuppressWarnings("unchecked") @@ -202,8 +218,9 @@ public class NMSBinding implements INMSBinding { try { IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); return (T) i.get(in); - } catch (IllegalAccessException e) { - e.printStackTrace(); + } catch (IllegalAccessException | RuntimeException e) { + throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "." + + i.getName() + " for " + returnType.getName(), e); } } } @@ -366,11 +383,18 @@ public class NMSBinding implements INMSBinding { } private RegistryAccess registry() { - return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + + if (access == null) { + throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version"); + } + + return access; } private Registry getCustomBiomeRegistry() { - return registry().lookup(Registries.BIOME).orElse(null); + return registry().lookup(Registries.BIOME).orElseThrow(() -> new IllegalStateException( + "Iris cannot resolve the Minecraft biome registry on this server version")); } private Registry getBlockRegistry() { @@ -435,7 +459,26 @@ public class NMSBinding implements INMSBinding { @Override public String getKeyForBiomeBase(Object biomeBase) { - return getCustomBiomeRegistry().getKey((net.minecraft.world.level.biome.Biome) biomeBase).getPath(); // something, not something:something + net.minecraft.world.level.biome.Biome biome; + if (biomeBase instanceof Holder holder) { + Object value = holder.value(); + if (!(value instanceof net.minecraft.world.level.biome.Biome held)) { + throw new IllegalArgumentException("Iris cannot read a biome key from holder value " + + (value == null ? "null" : value.getClass().getName())); + } + biome = held; + } else if (biomeBase instanceof net.minecraft.world.level.biome.Biome direct) { + biome = direct; + } else { + throw new IllegalArgumentException("Iris cannot read a biome key from " + + (biomeBase == null ? "null" : biomeBase.getClass().getName())); + } + + Identifier key = getCustomBiomeRegistry().getKey(biome); + if (key == null) { + throw new IllegalStateException("Iris found no registry key for biome " + biome); + } + return key.getPath(); // something, not something:something } @Override @@ -805,7 +848,7 @@ public class NMSBinding implements INMSBinding { @Override public MCAPaletteAccess createPalette() { - MCAIdMapper registry = registryCache.aquireNasty(() -> { + MCAIdMapper registry = registryCache.aquireNastyPrint(() -> { Field cf = IdMapper.class.getDeclaredField("tToId"); Field df = IdMapper.class.getDeclaredField("idToT"); Field bf = IdMapper.class.getDeclaredField("nextId"); @@ -818,7 +861,13 @@ public class NMSBinding implements INMSBinding { List d = (List) df.get(blockData); return new MCAIdMapper(c, d, b); }); - MCAPalette global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + if (registry == null) { + throw new IllegalStateException("Iris cannot mirror the Minecraft block state id map on this server version"); + } + MCAPalette global = globalCache.aquireNastyPrint(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + if (global == null) { + throw new IllegalStateException("Iris cannot build the global block state palette on this server version"); + } java.util.Map innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); java.util.Map outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java index 9497e86f5..f3aa5ceba 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorFailureContractTest.java @@ -91,6 +91,27 @@ public class IrisChunkGeneratorFailureContractTest { assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_worldgen_heightmaps\")")); } + @Test + public void worldgenHeightmapPrimingLivesInTheSharedNativegenSources() throws IOException { + Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent(); + Path shared = nativegen.resolve("WorldgenTerrainHeightmaps.java"); + + assertTrue("Worldgen heightmap priming must be shared with the modded loaders through " + + nativegen, Files.isRegularFile(shared)); + + String heightmaps = Files.readString(shared); + + assertTrue(heightmaps.contains("package art.arcane.iris.nativegen;")); + assertTrue(heightmaps.contains("public static void primeTerrain(")); + assertTrue(heightmaps.contains("public static void primeStructurePlacement(")); + assertFalse(heightmaps.contains("org.bukkit")); + assertFalse(heightmaps.contains("craftbukkit")); + + String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))); + + assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;")); + } + private static int occurrences(String source, String needle) { int count = 0; int index = source.indexOf(needle); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java index 5de4d4aa9..0633975c8 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java @@ -79,23 +79,32 @@ public class IrisChunkGeneratorMonumentLocateContractTest { @Test public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException { - String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))); + Path processor = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")); + String source = Files.readString(processor); + String foundation = Files.readString( + processor.resolveSibling("NativeStructureFoundationBuilder.java")); int placement = source.indexOf("start.placeInChunk(world, structureManager, generator"); int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement); - int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement); - int terrainFloor = source.indexOf("Math.max(terrainY,", stiltPlacement); + int stiltDefinition = foundation.indexOf("static void placeStilts("); + int occupancyCheck = foundation.indexOf("if (state.isSolid())", stiltDefinition); + int terrainFloor = foundation.indexOf("Math.max(terrainY,", stiltDefinition); assertTrue(placement >= 0); assertTrue(stiltPlacement > placement); - assertTrue(occupancyCheck > stiltPlacement); - assertTrue(terrainFloor > stiltPlacement); + assertTrue(stiltDefinition >= 0); + assertTrue(occupancyCheck > stiltDefinition); + assertTrue(terrainFloor > stiltDefinition); assertFalse(source.contains("state.equals(")); + assertFalse(foundation.contains("state.equals(")); assertFalse(source.contains("snapshot.states")); + assertFalse(foundation.contains("snapshot.states")); } @Test public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException { - String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))); + String source = Files.readString( + Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")) + .resolveSibling("NativeStructureVerticalPlacer.java")); int placementStart = source.indexOf("public static int applyVerticalPlacement"); int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart); int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java index 91bc2797c..0dc062e2f 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmapsTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.nms.v26_2_R1; +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; import com.mojang.serialization.Codec; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java index e8e735008..801bd2721 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureFactoryTest.java @@ -95,7 +95,7 @@ public class NativeStructureFactoryTest { new PiecesContainer(List.of(piece)) ); - StructureStart relocated = NativeStructurePostProcessor.relocateToMinY( + StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY( start, source, -20, LevelHeightAccessor.create(-64, 384)); assertNotSame(start, relocated); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java index efe787fde..d16da49c7 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorEncaseTest.java @@ -61,7 +61,7 @@ public class NativeStructurePostProcessorEncaseTest { put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState()); put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState()); - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), area, "minecraft:stronghold", start, new IrisStructureTerrain() .setMode(IrisStructureTerrainMode.ENCASE) @@ -86,7 +86,7 @@ public class NativeStructurePostProcessorEncaseTest { BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); Map blocks = new HashMap<>(); - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), bounds, "minecraft:stronghold", start, new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE), null); @@ -105,7 +105,7 @@ public class NativeStructurePostProcessorEncaseTest { Map blocks = new HashMap<>(); IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff"); - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), bounds, "minecraft:stronghold", start, new IrisStructureTerrain() .setMode(IrisStructureTerrainMode.ENCASE) @@ -131,7 +131,7 @@ public class NativeStructurePostProcessorEncaseTest { @Test public void buryAndEncapsulateAdaptationsAutoDefaultToEncase() { for (TerrainAdjustment adjustment : List.of(TerrainAdjustment.BURY, TerrainAdjustment.ENCAPSULATE)) { - IrisStructureTerrain resolved = NativeStructurePostProcessor.resolveNativeTerrain( + IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain( start(adjustment, 64), null); assertEquals(IrisStructureTerrainMode.ENCASE, resolved.resolvedMode()); @@ -146,7 +146,7 @@ public class NativeStructurePostProcessorEncaseTest { public void otherAdaptationsNeverAutoDefaultToEncase() { for (TerrainAdjustment adjustment : List.of( TerrainAdjustment.NONE, TerrainAdjustment.BEARD_THIN, TerrainAdjustment.BEARD_BOX)) { - assertNull(NativeStructurePostProcessor.resolveNativeTerrain( + assertNull(NativeStructureTerrainIntegrator.resolveNativeTerrain( start(adjustment, 64), null)); } } @@ -156,7 +156,7 @@ public class NativeStructurePostProcessorEncaseTest { IrisStructureTerrain configured = new IrisStructureTerrain() .setMode(IrisStructureTerrainMode.SOURCE); - assertSame(configured, NativeStructurePostProcessor.resolveNativeTerrain( + assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain( start(TerrainAdjustment.BURY, 64), configured)); } @@ -189,9 +189,9 @@ public class NativeStructurePostProcessorEncaseTest { StructureStart first = start(TerrainAdjustment.BURY, 64); StructureStart second = start(TerrainAdjustment.BURY, 64); - NativeStructurePostProcessor.applyVerticalShift( + NativeStructureVerticalPlacer.applyVerticalShift( first, -64, -256, 320, true, false, band, (x, z) -> 40); - NativeStructurePostProcessor.applyVerticalShift( + NativeStructureVerticalPlacer.applyVerticalShift( second, -64, -256, 320, true, false, band, (x, z) -> 40); BoundingBox bounds = first.getBoundingBox(); @@ -199,7 +199,7 @@ public class NativeStructurePostProcessorEncaseTest { assertTrue(bounds.minY() >= -120); assertTrue(bounds.maxY() <= -20); - int repeated = NativeStructurePostProcessor.applyVerticalShift( + int repeated = NativeStructureVerticalPlacer.applyVerticalShift( first, -64, -256, 320, true, false, band, (x, z) -> 40); assertEquals(0, repeated); } @@ -209,7 +209,7 @@ public class NativeStructurePostProcessorEncaseTest { IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45); StructureStart start = start(TerrainAdjustment.BURY, 64); - NativeStructurePostProcessor.applyVerticalShift( + NativeStructureVerticalPlacer.applyVerticalShift( start, 0, -256, 320, true, false, band, (x, z) -> 40); BoundingBox bounds = start.getBoundingBox(); @@ -225,7 +225,7 @@ public class NativeStructurePostProcessorEncaseTest { BoundingBox bounds = start.getBoundingBox(); int height = bounds.maxY() - bounds.minY(); - NativeStructurePostProcessor.applyVerticalShift( + NativeStructureVerticalPlacer.applyVerticalShift( start, -200, -256, 320, true, false, band, (x, z) -> 40); assertEquals(-64 - height / 2, start.getBoundingBox().minY()); @@ -237,7 +237,7 @@ public class NativeStructurePostProcessorEncaseTest { StructureStart start = start(TerrainAdjustment.BURY, 64); int minY = start.getBoundingBox().minY(); - int offset = NativeStructurePostProcessor.applyVerticalShift( + int offset = NativeStructureVerticalPlacer.applyVerticalShift( start, 0, -256, 320, true, true, band, (x, z) -> 40); assertEquals(0, offset); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java index ef66c2ed8..30379009e 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorMonumentTest.java @@ -33,7 +33,7 @@ public class NativeStructurePostProcessorMonumentTest { public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() { StructureStart start = monumentStart(1337L); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0); assertEquals(0, offset); @@ -45,11 +45,11 @@ public class NativeStructurePostProcessorMonumentTest { public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() { StructureStart start = monumentStart(1337L); OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start); - List children = NativeStructurePostProcessor.monumentChildPieces(building); + List children = NativeStructureVerticalPlacer.monumentChildPieces(building); int[] childMinY = minimumYs(children); BoundingBox cachedBounds = start.getBoundingBox(); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); assertEquals(-13, offset); @@ -63,7 +63,7 @@ public class NativeStructurePostProcessorMonumentTest { assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY()); } - int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement( + int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); assertEquals(0, repeatedOffset); } @@ -72,7 +72,7 @@ public class NativeStructurePostProcessorMonumentTest { public void configuredOffsetIsRelativeToTheActualSeaLevel() { StructureStart start = monumentStart(1337L); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0); assertEquals(-10, offset); @@ -85,7 +85,7 @@ public class NativeStructurePostProcessorMonumentTest { long seed = 1337L; ChunkPos chunkPos = new ChunkPos(0, 0); StructureStart initial = monumentStart(seed); - NativeStructurePostProcessor.applyVerticalPlacement( + NativeStructureVerticalPlacer.applyVerticalPlacement( initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad( @@ -94,7 +94,7 @@ public class NativeStructurePostProcessorMonumentTest { monumentStructure(), chunkPos, 0, regenerated); assertEquals(39, reloaded.getBoundingBox().minY()); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0); assertEquals(-13, offset); assertEquals(26, reloaded.getBoundingBox().minY()); @@ -105,7 +105,7 @@ public class NativeStructurePostProcessorMonumentTest { public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() { StructureStart start = monumentStart(1337L); try { - NativeStructurePostProcessor.applyVerticalPlacement( + NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0); } catch (IllegalStateException error) { assertTrue(error.getMessage().contains("cannot align")); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java index c6b8b637a..775202bc3 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorScatteredFeatureTest.java @@ -45,7 +45,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest { BoundingBox cachedBounds = start.getBoundingBox(); BoundingBox footprint = piece.getBoundingBox(); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92); @@ -63,7 +63,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest { StructureStart start = jungleStart(piece); BoundingBox footprint = piece.getBoundingBox(); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null, (x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70); @@ -78,9 +78,9 @@ public class NativeStructurePostProcessorScatteredFeatureTest { TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0); StructureStart start = desertStart(piece); - int initialOffset = NativeStructurePostProcessor.applyVerticalPlacement( + int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80); - int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement( + int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80); assertEquals(17, initialOffset); @@ -93,7 +93,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest { TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0); StructureStart start = desertStart(piece); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318); assertEquals(241, offset); @@ -110,12 +110,12 @@ public class NativeStructurePostProcessorScatteredFeatureTest { StructureStart jungleStart = jungleStart(jungle); StructureStart swampStart = swampStart(swamp); - assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(desertStart)); - assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(jungleStart)); - assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(swampStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart)); + assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart)); AtomicInteger terrainQueries = new AtomicInteger(); - int offset = NativeStructurePostProcessor.applyVerticalPlacement( + int offset = NativeStructureVerticalPlacer.applyVerticalPlacement( swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null, (x, z) -> terrainQueries.incrementAndGet()); assertEquals(0, offset); @@ -125,7 +125,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest { @Test public void scatteredHeightFieldMatchesTheRuntimeContract() { - Field field = NativeStructurePostProcessor.resolveScatteredHeightPositionField(); + Field field = NativeStructureReflection.resolveScatteredHeightPositionField(); assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass()); assertEquals(int.class, field.getType()); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java index 197a76329..53e492b47 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java @@ -61,13 +61,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { @Test public void onlySurfaceBeardThinStructuresPrepareTerrain() { - assertTrue(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain( + assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES)); - assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain( + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES)); - assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain( + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.SURFACE_STRUCTURES)); - assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain( + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES)); } @@ -78,86 +78,86 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { TerrainAdjustment.BURY, TerrainAdjustment.BEARD_BOX, TerrainAdjustment.ENCAPSULATE)) { - assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain( + assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain( adjustment, GenerationStep.Decoration.UNDERGROUND_STRUCTURES)); } } @Test public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() { - NativeStructurePostProcessor.SurfaceAnchor anchor = anchor(80, 2); + NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(80, 2); - assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(anchor), 2, 2, 64)); - assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(anchor), 16, 2, 64)); - assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(anchor), 17, 2, 64)); } @Test public void surfaceAnchorRaisesAndLowersThroughTheTaper() { - NativeStructurePostProcessor.SurfaceAnchor raised = anchor(80, 2); - NativeStructurePostProcessor.SurfaceAnchor lowered = anchor(64, 2); + NativeStructureSurfaceFitter.SurfaceAnchor raised = anchor(80, 2); + NativeStructureSurfaceFitter.SurfaceAnchor lowered = anchor(64, 2); - assertEquals(68, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(68, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(raised), 10, 2, 64)); - assertEquals(76, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(76, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(lowered), 10, 2, 80)); } @Test public void containingRigidFloorsHaveDeterministicPriority() { - NativeStructurePostProcessor.SurfaceAnchor rigid = anchor(70, 2); - NativeStructurePostProcessor.SurfaceAnchor junction = anchor(90, 1); + NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(70, 2); + NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(90, 1); - assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(rigid, junction), 2, 2, 64)); - assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(junction, rigid), 2, 2, 64)); - NativeStructurePostProcessor.SurfaceAnchor weakTie = anchor(48, 1); - NativeStructurePostProcessor.SurfaceAnchor strongTie = anchor(80, 2); - assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget( + NativeStructureSurfaceFitter.SurfaceAnchor weakTie = anchor(48, 1); + NativeStructureSurfaceFitter.SurfaceAnchor strongTie = anchor(80, 2); + assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(weakTie, strongTie), 2, 2, 64)); - assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(strongTie, weakTie), 2, 2, 64)); } @Test public void containingFootprintOverridesAnAdjacentPiecesFalloff() { - NativeStructurePostProcessor.SurfaceAnchor local = - new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, 65, 2); - NativeStructurePostProcessor.SurfaceAnchor adjacent = - new NativeStructurePostProcessor.SurfaceAnchor(5, 9, 0, 4, 80, 2); + NativeStructureSurfaceFitter.SurfaceAnchor local = + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, 65, 2); + NativeStructureSurfaceFitter.SurfaceAnchor adjacent = + new NativeStructureSurfaceFitter.SurfaceAnchor(5, 9, 0, 4, 80, 2); - assertEquals(77, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(77, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(adjacent), 4, 2, 64)); - assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(local, adjacent), 4, 2, 64)); - assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(adjacent, local), 4, 2, 64)); } @Test public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() { - NativeStructurePostProcessor.SurfaceAnchor high = - new NativeStructurePostProcessor.SurfaceAnchor(0, 0, 0, 0, 80, 2); - NativeStructurePostProcessor.SurfaceAnchor low = - new NativeStructurePostProcessor.SurfaceAnchor(12, 12, 0, 0, 48, 2); - int previous = NativeStructurePostProcessor.resolveSurfaceTarget( + NativeStructureSurfaceFitter.SurfaceAnchor high = + new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2); + NativeStructureSurfaceFitter.SurfaceAnchor low = + new NativeStructureSurfaceFitter.SurfaceAnchor(12, 12, 0, 0, 48, 2); + int previous = NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(high, low), 0, 0, 64); for (int x = 1; x <= 12; x++) { - int forward = NativeStructurePostProcessor.resolveSurfaceTarget( + int forward = NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(high, low), x, 0, 64); - int reversed = NativeStructurePostProcessor.resolveSurfaceTarget( + int reversed = NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(low, high), x, 0, 64); assertEquals(forward, reversed); assertTrue(Math.abs(forward - previous) <= 4); previous = forward; } - assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget( + assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget( List.of(high, low), 6, 0, 64)); } @@ -170,7 +170,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState()); - NativeStructurePostProcessor.applySurfaceColumn( + NativeStructureSurfaceFitter.applySurfaceColumn( world(lowered), new BlockPos.MutableBlockPos(), 0, 0, 64, 62, -64, 319); @@ -185,7 +185,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState()); - NativeStructurePostProcessor.applySurfaceColumn( + NativeStructureSurfaceFitter.applySurfaceColumn( world(raised), new BlockPos.MutableBlockPos(), 0, 0, 64, 68, -64, 319); @@ -205,7 +205,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { put(blocks, 0, 66, 0, log); put(blocks, 0, 68, 0, leaves); - NativeStructurePostProcessor.applySurfaceColumn( + NativeStructureSurfaceFitter.applySurfaceColumn( world(blocks), new BlockPos.MutableBlockPos(), 0, 0, 64, 68, -64, 319); @@ -221,7 +221,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState()); put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState()); - NativeStructurePostProcessor.applySurfaceColumn( + NativeStructureSurfaceFitter.applySurfaceColumn( world(blocks), new BlockPos.MutableBlockPos(), 0, 0, 64, 62, -64, 319); @@ -236,7 +236,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { StructureStart start = desertStart(); int minY = start.getBoundingBox().minY(); - int offset = NativeStructurePostProcessor.applyVerticalShift( + int offset = NativeStructureVerticalPlacer.applyVerticalShift( start, 0, -64, 320, true, true, null, (x, z) -> 40); assertEquals(0, offset); @@ -248,7 +248,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { StructureStart start = desertStart(); int minY = start.getBoundingBox().minY(); - int offset = NativeStructurePostProcessor.applyVerticalShift( + int offset = NativeStructureVerticalPlacer.applyVerticalShift( start, -8, -64, 320, true, true, null, (x, z) -> 40); assertEquals(-8, offset); @@ -262,7 +262,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { int maxY = start.getBoundingBox().maxY(); int expected = 40 - 1 - maxY; - int offset = NativeStructurePostProcessor.applyVerticalShift( + int offset = NativeStructureVerticalPlacer.applyVerticalShift( start, 0, -64, 320, true, false, null, (x, z) -> 40); assertEquals(expected, offset); @@ -275,7 +275,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { int minY = start.getBoundingBox().minY(); int worldMinY = minY - 4; - int offset = NativeStructurePostProcessor.applyVerticalShift( + int offset = NativeStructureVerticalPlacer.applyVerticalShift( start, 0, worldMinY, 320, true, false, null, (x, z) -> worldMinY); assertEquals(-4, offset); @@ -289,7 +289,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { Map blocks = new HashMap<>(); put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.STONE.defaultBlockState()); - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), bounds, "minecraft:desert_pyramid", start, new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null); @@ -310,7 +310,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { put(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ(), Blocks.STONE.defaultBlockState()); - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), area, "minecraft:desert_pyramid", start, new IrisStructureTerrain() .setMode(IrisStructureTerrainMode.FORCE_CARVE) @@ -345,7 +345,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { put(blocks, firstBounds.minX(), y, z, Blocks.STONE.defaultBlockState()); put(blocks, gapX, y, z, Blocks.STONE.defaultBlockState()); - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), area, "minecraft:ancient_city", start, new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null); @@ -372,7 +372,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { new BlockPos(1, 2, 0), Blocks.DEEPSLATE.defaultBlockState(), null))); Map columns = new HashMap<>(); - assertTrue(NativeStructurePostProcessor.emitTemplateColumns( + assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns( List.of(template), new BlockPos(0, 0, 0), Rotation.NONE, new BoundingBox(0, 0, 0, 1, 2, 0), (x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL, @@ -392,7 +392,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { new BlockPos(0, 1, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null))); Map columns = new HashMap<>(); - assertTrue(NativeStructurePostProcessor.emitTemplateColumns( + assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns( List.of(template), new BlockPos(0, 0, 0), Rotation.NONE, new BoundingBox(0, 0, 0, 0, 1, 0), (x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL, @@ -406,7 +406,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { StructureStart start = desertStart(); BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); - StructureCarvingFootprint footprint = NativeStructurePostProcessor.carveFootprint( + StructureCarvingFootprint footprint = NativeStructureTerrainIntegrator.carveFootprint( start, 4, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); assertEquals(bounds.minX() - 4, footprint.minX()); @@ -423,13 +423,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { public void carveFootprintIsComputedOncePerStartAndPadding() { StructureStart start = desertStart(); - StructureCarvingFootprint first = NativeStructurePostProcessor.carveFootprint( + StructureCarvingFootprint first = NativeStructureTerrainIntegrator.carveFootprint( start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); - StructureCarvingFootprint repeated = NativeStructurePostProcessor.carveFootprint( + StructureCarvingFootprint repeated = NativeStructureTerrainIntegrator.carveFootprint( start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); - StructureCarvingFootprint widened = NativeStructurePostProcessor.carveFootprint( + StructureCarvingFootprint widened = NativeStructureTerrainIntegrator.carveFootprint( start, 7, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); - StructureCarvingFootprint other = NativeStructurePostProcessor.carveFootprint( + StructureCarvingFootprint other = NativeStructureTerrainIntegrator.carveFootprint( desertStart(), 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager); assertSame(first, repeated); @@ -441,13 +441,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { public void organicCarveNeverCutsBelowTheColumnSupportingFloor() { StructureStart start = desertStart(); BoundingBox bounds = start.getPieces().getFirst().getBoundingBox(); - NativeStructurePostProcessor.OrganicCarve carve = organicCarve(start, 6); + NativeStructureTerrainIntegrator.OrganicCarve carve = organicCarve(start, 6); BoundingBox area = new BoundingBox( bounds.minX() - 6, bounds.minY() - 4, bounds.minZ() - 6, bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6); Map blocks = fill(area); - NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area, carve); + NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, carve); int centerX = bounds.minX() + bounds.getXSpan() / 2; int centerZ = bounds.minZ() + bounds.getZSpan() / 2; @@ -471,9 +471,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { Map uniformBlocks = fill(area); Map lobedBlocks = fill(area); - NativeStructurePostProcessor.carveOrganicColumns( + NativeStructureTerrainIntegrator.carveOrganicColumns( world(uniformBlocks), area, organicCarve(start, 10, 0D)); - NativeStructurePostProcessor.carveOrganicColumns( + NativeStructureTerrainIntegrator.carveOrganicColumns( world(lobedBlocks), area, organicCarve(start, 10, 0.85D)); int uniform = 0; @@ -521,9 +521,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { Map narrowBlocks = fill(narrow); // Each chunk context rebuilds its own noise channels from the shared start identity. - NativeStructurePostProcessor.carveOrganicColumns( + NativeStructureTerrainIntegrator.carveOrganicColumns( world(wideBlocks), wide, organicCarve(start, 6)); - NativeStructurePostProcessor.carveOrganicColumns( + NativeStructureTerrainIntegrator.carveOrganicColumns( world(narrowBlocks), narrow, organicCarve(start, 6)); int carved = 0; @@ -552,7 +552,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { int centerX = bounds.minX() + bounds.getXSpan() / 2; int centerZ = bounds.minZ() + bounds.getZSpan() / 2; - NativeStructurePostProcessor.integrateTerrain( + NativeStructureTerrainIntegrator.integrateTerrain( world(blocks), area, "minecraft:ancient_city", start, new IrisStructureTerrain() .setMode(IrisStructureTerrainMode.FORCE_CARVE) @@ -572,21 +572,21 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { @Test public void sparseStiltGridIsDeterministicAndPreflightsGround() { - assertTrue(NativeStructurePostProcessor.isStiltColumn(0, 0, 4)); - assertTrue(NativeStructurePostProcessor.isStiltColumn(-4, 8, 4)); - assertFalse(NativeStructurePostProcessor.isStiltColumn(1, 0, 4)); - assertTrue(NativeStructurePostProcessor.isStiltColumn(1, 1, 1)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(0, 0, 4)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(-4, 8, 4)); + assertFalse(NativeStructureFoundationBuilder.isStiltColumn(1, 0, 4)); + assertTrue(NativeStructureFoundationBuilder.isStiltColumn(1, 1, 1)); Map blocks = new HashMap<>(); put(blocks, 0, 7, 0, Blocks.DEEPSLATE.defaultBlockState()); put(blocks, 0, 8, 0, Blocks.SCULK_VEIN.defaultBlockState()); BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - assertEquals(7, NativeStructurePostProcessor.findStiltAnchorY( + assertEquals(7, NativeStructureFoundationBuilder.findStiltAnchorY( world(blocks), 0, 0, 10, 2, -64, -64, position)); - assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY( + assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY( world(blocks), 0, 0, 10, 1, -64, -64, position)); - assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY( + assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY( world(new HashMap<>()), 0, 0, 10, 64, -64, -64, position)); } @@ -635,7 +635,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { @Test public void singlePoolTemplateFieldMatchesTheRuntimeContract() { - Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField(); + Field field = NativeStructureReflection.resolveSinglePoolTemplateField(); assertEquals(SinglePoolElement.class, field.getDeclaringClass()); assertEquals(Either.class, field.getType()); @@ -649,12 +649,12 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { public void runtimeTemplatesAndLegacyAirUseTheExactContract() { StructureTemplate runtimeTemplate = new StructureTemplate(); - assertEquals(runtimeTemplate, NativeStructurePostProcessor.resolveTemplateReference( + assertEquals(runtimeTemplate, NativeStructureReflection.resolveTemplateReference( Either.right(runtimeTemplate), null)); - assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(79, 80, false)); - assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(80, 80, false)); - assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, false)); - assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, true)); + assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(79, 80, false)); + assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(80, 80, false)); + assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, false)); + assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, true)); } @Test @@ -687,7 +687,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState()); blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState()); - NativeStructurePostProcessor.clearTemplateAir( + NativeStructureTerrainIntegrator.clearTemplateAir( world(blocks), template, origin, 80, settings); assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition)); @@ -699,11 +699,11 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() { BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15); - assertTrue(NativeStructurePostProcessor.intersects( + assertTrue(NativeStructureTerrainIntegrator.intersects( new BoundingBox(15, 60, 15, 30, 90, 30), area)); - assertFalse(NativeStructurePostProcessor.intersects( + assertFalse(NativeStructureTerrainIntegrator.intersects( new BoundingBox(16, 60, 16, 30, 90, 30), area)); - assertFalse(NativeStructurePostProcessor.intersects( + assertFalse(NativeStructureTerrainIntegrator.intersects( new BoundingBox(0, 320, 0, 15, 350, 15), area)); } @@ -717,23 +717,22 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { BlockState log = Blocks.OAK_LOG.defaultBlockState(); blocks.put(origin, log); - NativeStructurePostProcessor.clearTemplateAir(world(blocks), template, origin, 80, settings); + NativeStructureTerrainIntegrator.clearTemplateAir(world(blocks), template, origin, 80, settings); assertEquals(log, blocks.get(origin)); } - private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) { - return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength); + private static NativeStructureSurfaceFitter.SurfaceAnchor anchor(int meetY, int strength) { + return new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, meetY, strength); } - private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start, - int horizontalPadding) { + private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve( + StructureStart start, int horizontalPadding) { return organicCarve(start, horizontalPadding, 0.85D); } - private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start, - int horizontalPadding, - double lobeStrength) { + private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve( + StructureStart start, int horizontalPadding, double lobeStrength) { IrisStructureTerrain terrain = new IrisStructureTerrain() .setMode(IrisStructureTerrainMode.FORCE_CARVE) .setShape(IrisStructureCarveShape.ERODED) @@ -743,8 +742,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { .setErosionStrength(1D) .setErosionFrequency(0.05D) .setLobeStrength(lobeStrength); - return NativeStructurePostProcessor.organicCarve( - NativeStructurePostProcessor.carveFootprint(start, horizontalPadding, + return NativeStructureTerrainIntegrator.organicCarve( + NativeStructureTerrainIntegrator.carveFootprint(start, horizontalPadding, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager), terrain, IrisStructureCarveShape.ERODED, TEST_SEED); } @@ -778,8 +777,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { SLAB_WIDTH - 1 + SLAB_PADDING, transectY, SLAB_DEPTH - 1); Map blocks = fill(area); - NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area, - NativeStructurePostProcessor.organicCarve( + NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, + NativeStructureTerrainIntegrator.organicCarve( footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED)); int[] depths = new int[SLAB_DEPTH]; diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java index c34b85738..140008f5d 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java @@ -11,53 +11,53 @@ import static org.junit.Assert.assertTrue; public class NativeStructurePostProcessorVegetationTest { @Test public void surfaceStructuresClearTreeColumnsAutomatically() { - assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false)); - assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false)); + assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(100, 100, false)); + assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(116, 100, false)); } @Test public void buriedStructuresPreserveUnrelatedSurfaceForest() { - assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false)); - assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false)); + assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(99, 100, false)); + assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, false)); } @Test public void explicitVegetationOptionForcesUnusualPlacementCleanup() { - assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true)); + assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, true)); } @Test public void surfaceStructuresPreserveVegetationUnlessConfigured() { - assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( + assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint( GenerationStep.Decoration.SURFACE_STRUCTURES, false)); - assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( + assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint( GenerationStep.Decoration.SURFACE_STRUCTURES, true)); } @Test public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() { - assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( + assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint( GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false)); - assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( + assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint( GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true)); } @Test public void allUndergroundGenerationStepsShareOneClassification() { - assertTrue(NativeStructurePostProcessor.isUndergroundStep( + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( GenerationStep.Decoration.UNDERGROUND_STRUCTURES)); - assertTrue(NativeStructurePostProcessor.isUndergroundStep( + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( GenerationStep.Decoration.UNDERGROUND_DECORATION)); - assertTrue(NativeStructurePostProcessor.isUndergroundStep( + assertTrue(NativeStructureVegetationClearer.isUndergroundStep( GenerationStep.Decoration.STRONGHOLDS)); - assertFalse(NativeStructurePostProcessor.isUndergroundStep( + assertFalse(NativeStructureVegetationClearer.isUndergroundStep( GenerationStep.Decoration.SURFACE_STRUCTURES)); } @Test public void undergroundStructuresUseTheLowestTerrainColumn() { BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0); - int offset = NativeStructurePostProcessor.resolveBuriedOffset( + int offset = NativeStructureVerticalPlacer.resolveBuriedOffset( bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100); assertEquals(-5, offset); } @@ -65,7 +65,7 @@ public class NativeStructurePostProcessorVegetationTest { @Test public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() { BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0); - assertEquals(-2, NativeStructurePostProcessor.resolveBuriedOffset( + assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset( bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100)); } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index ba2be9d8a..6fe36df87 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -27,28 +27,23 @@ import art.arcane.iris.engine.framework.EnginePlatformHooks; import art.arcane.iris.engine.framework.EngineWorldManagerProvider; import art.arcane.iris.core.splash.IrisSplashComposer; import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.core.IrisWorldStorage; -import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.BukkitWorldReconciler; +import art.arcane.iris.core.IrisWorldGeneratorResolver; +import art.arcane.iris.core.PendingWorldDeleteQueue; +import art.arcane.iris.core.SettingsHotloadWatch; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.lifecycle.PaperLibBootstrap; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks; -import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; import art.arcane.iris.core.runtime.WorldRuntimeControlService; -import art.arcane.iris.core.lifecycle.WorldLifecycleStaging; import art.arcane.iris.api.terrain.IrisTerrainService; import art.arcane.iris.core.link.IrisPapiInstaller; import art.arcane.iris.core.link.IrisPapiListener; import art.arcane.iris.core.link.IrisPapiState; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.link.MultiverseCoreLink; -import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; -import art.arcane.iris.core.pack.BrokenPackException; -import art.arcane.iris.core.pack.PackValidationRegistry; -import art.arcane.iris.core.pack.PackValidationResult; -import art.arcane.iris.core.pack.PackValidator; import art.arcane.iris.core.gui.BukkitGuiHost; import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.service.EditSVC; @@ -61,19 +56,14 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.TreeBlockMaterial; import art.arcane.iris.engine.object.IrisCompat; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisWorld; -import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.core.safeguard.IrisSafeguard; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.platform.bukkit.BukkitPlatform; -import art.arcane.iris.platform.bukkit.BukkitEnvironment; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.LogLevel; import art.arcane.volmlib.integration.ReloadAware; -import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -96,17 +86,13 @@ import art.arcane.iris.util.common.plugin.VolmitPlugin; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.chunk.ChunkTickets; import art.arcane.iris.util.common.scheduling.J; -import art.arcane.iris.util.common.misc.ServerProperties; import art.arcane.iris.util.simd.SimdSupport; import art.arcane.volmlib.util.scheduling.Queue; import art.arcane.volmlib.util.scheduling.ShurikenQueue; -import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.NamespacedKey; import org.bukkit.World; -import org.bukkit.WorldCreator; import org.bukkit.block.data.BlockData; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -121,23 +107,14 @@ import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.BufferedInputStream; -import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; -import java.net.URI; import java.util.ArrayList; -import java.util.Collection; import java.util.Date; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -145,7 +122,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -165,8 +141,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { public static ChunkTickets tickets; private static VolmitSender sender; private static Thread shutdownHook; - private static File settingsFile; - private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt"; private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance(); static { try { @@ -178,11 +152,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } } + private static final Object TEARDOWN_LOCK = new Object(); private final AtomicBoolean alreadyDrained = new AtomicBoolean(false); + private final AtomicBoolean servicesDisabled = new AtomicBoolean(false); + private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false); private volatile PlaceholderRegistration papiRegistration; private volatile IrisPapiListener papiListener; private volatile IrisPapiState papiState; private KMap, IrisService> services; + private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this); + private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this); + private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this); + private volatile SettingsHotloadWatch settingsHotloadWatch; public static VolmitSender getSender() { if (sender == null) { @@ -292,69 +273,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } } - public static File getCached(String name, String url) { - String h = IO.hash(name + "@" + url); - File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h); - - if (!f.exists()) { - try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - Iris.verbose("Aquiring " + name); - } - } catch (IOException e) { - Iris.reportError(e); - } - } - - return f.exists() ? f : null; - } - - public static String getNonCached(String name, String url) { - String h = IO.hash(name + "*" + url); - File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h); - - try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } - } catch (IOException e) { - Iris.reportError(e); - } - - try { - return IO.readAll(f); - } catch (IOException e) { - Iris.reportError(e); - } - - return ""; - } - - public static File getNonCachedFile(String name, String url) { - String h = IO.hash(name + "*" + url); - File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h); - Iris.verbose("Download " + name + " -> " + url); - try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } - - fileOutputStream.flush(); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - - return f; - } - public static void warn(String format, Object... objs) { msg(C.YELLOW + safeFormat(format, objs)); } @@ -616,6 +534,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { private void enable() { alreadyDrained.set(false); + servicesDisabled.set(false); + sharedRuntimeClosed.set(false); + MultiBurst.burst.reopen(); + MultiBurst.ioBurst.reopen(); IrisLanguage.initialize(); PaperLibBootstrap.install(); SimdSupport.install(); @@ -635,7 +557,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { IrisServices.register(IrisCompat.class, compat); ServerConfigurator.configure(); IrisToolbelt.applyPregenPerformanceProfile(); - validateAllPacks(); + generatorResolver.validateAllPacks(); IrisSafeguard.execute(); getSender().setTag(getTag()); splash(); @@ -649,37 +571,38 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks()); IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) IrisWorldManager::new); - IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup); - settingsFile = getDataFile("settings.json"); + IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) pendingWorldDeletes::queueWorldDeletionOnStartup); + SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json")); + settingsHotloadWatch = watch; configHotloadEngine = new ConfigHotloadEngine( - Iris::isSettingsFile, - Iris::knownSettingsFiles, - Iris::readSettingsContent, - Iris::normalizeSettingsContent + watch::isSettingsFile, + watch::knownSettingsFiles, + watch::readSettingsContent, + watch::normalizeSettingsContent ); - configHotloadEngine.configure(3_000L, List.of(settingsFile), List.of()); + configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of()); services.values().forEach(IrisService::onEnable); services.values().forEach(this::registerListener); addShutdownHook(); - processPendingStartupWorldDeletes(); + pendingWorldDeletes.processPendingStartupWorldDeletes(); WorldLifecycleService.get(); WorldRuntimeControlService.get(); if (J.isFolia()) { - J.s(() -> checkForBukkitWorlds(s -> true), 1); + J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1); } J.s(() -> { J.a(() -> IO.delete(getTemp())); J.a(this::bstats); - J.ar(this::checkConfigHotload, 60); + J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60); J.sr(this::tickQueue, 0); J.s(this::setupPapi); J.a(DatapackIngestService::autoIngestOnStartup, 60); autoStartStudio(); if (!J.isFolia()) { - checkForBukkitWorlds(s -> true); + worldReconciler.checkForBukkitWorlds(s -> true); } IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName()); IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName()); @@ -696,24 +619,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { return; } } - shutdownHook = new Thread(() -> { - if (alreadyDrained.compareAndSet(false, true)) { - try { - Bukkit.getWorlds() - .stream() - .map(World::getGenerator) - .filter(PlatformChunkGenerator.class::isInstance) - .map(PlatformChunkGenerator.class::cast) - .forEach(PlatformChunkGenerator::close); - } catch (Throwable e) { - Iris.reportError("Failed to close Iris world generators from the JVM shutdown hook.", e); - } - } - - MultiBurst.burst.close(); - MultiBurst.ioBurst.close(); - IrisServices.clear(); - }, "Iris-ShutdownHook"); + shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook"); try { Runtime.getRuntime().addShutdownHook(shutdownHook); } catch (IllegalStateException ex) { @@ -721,247 +627,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } } - public void checkForBukkitWorlds(Predicate filter) { - try { - KList deferredStartupWorlds = new KList<>(); - IrisWorlds.readBukkitWorlds().forEach((s, generator) -> { - try { - NamespacedKey worldKey = IrisWorldStorage.keyFromName(s); - if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return; - - Iris.info("Loading World: %s | Generator: %s", s, generator); - ChunkGenerator gen = getDefaultWorldGenerator(s, generator); - IrisDimension dim = loadDimension(s, generator); - assert dim != null && gen != null; - - Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "..."); - WorldCreator c = WorldCreator.ofKey(worldKey) - .generator(gen) - .environment(BukkitEnvironment.from(dim.getEnvironment())); - Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s); - if (stagedSeed != null) { - c.seed(stagedSeed); - } - INMS.get().createWorld(c); - Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!"); - } catch (Throwable e) { - if (containsCreateWorldUnsupportedOperation(e)) { - if (J.isFolia()) { - if (!deferredStartupWorlds.contains(s)) { - deferredStartupWorlds.add(s); - } - return; - } - Iris.error("Failed to load world " + s + "!"); - Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase."); - Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml."); - reportError("Failed to load staged startup world \"" + s + "\".", e); - return; - } - reportError("Failed to load startup world \"" + s + "\".", e); - } - }); - if (!deferredStartupWorlds.isEmpty()) { - Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds)); - Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution()); - } - } catch (Throwable e) { - reportError("Failed while loading startup Iris worlds.", e); - } - } - - private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { - Throwable cursor = throwable; - while (cursor != null) { - if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) { - for (StackTraceElement element : cursor.getStackTrace()) { - if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName()) - && "createWorld".equals(element.getMethodName())) { - return true; - } - } - } - cursor = cursor.getCause(); - } - return false; - } - - public static synchronized int queueWorldDeletionOnStartup(Collection worldNames) throws IOException { - if (instance == null || worldNames == null || worldNames.isEmpty()) { - return 0; - } - - LinkedHashMap queue = loadPendingWorldDeleteMap(); - int before = queue.size(); - - for (String worldName : worldNames) { - String normalized = normalizeWorldName(worldName); - if (normalized == null) { - continue; - } - queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized); - } - - if (queue.size() != before) { - writePendingWorldDeleteMap(queue); - } - - return queue.size() - before; - } - - private void processPendingStartupWorldDeletes() { - try { - try { - int unregistered = art.arcane.iris.core.tools.IrisCreator.removeTransientStudioWorldsFromBukkitYml(); - if (unregistered > 0) { - Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup."); - } - } catch (Throwable e) { - Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e); - } - - LinkedHashMap queue = loadPendingWorldDeleteMap(); - for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) { - queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld); - } - if (queue.isEmpty()) { - return; - } - - LinkedHashMap remaining = new LinkedHashMap<>(); - for (String worldName : queue.values()) { - if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) { - Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name."); - continue; - } - - NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName); - World loaded = WorldIdentity.resolve(worldKey).orElse(null); - if (loaded != null) { - if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) { - try { - PlatformChunkGenerator generator = IrisToolbelt.access(loaded); - if (generator != null) { - generator.close(); - } - IrisToolbelt.evacuate(loaded); - Bukkit.unloadWorld(loaded, false); - Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion."); - } catch (Throwable e) { - Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e); - } - - if (WorldIdentity.resolve(worldKey).isPresent()) { - Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup."); - remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); - continue; - } - } else { - Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded."); - remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); - continue; - } - } - - boolean foundAny = false; - boolean deletedAll = true; - for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { - File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName); - if (!worldFolder.exists()) { - continue; - } - - foundAny = true; - IO.delete(worldFolder); - if (worldFolder.exists()) { - deletedAll = false; - Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup."); - } else { - Iris.info("Deleted queued world folder \"" + familyWorldName + "\"."); - } - } - - if (!foundAny) { - Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing)."); - continue; - } - - if (!deletedAll) { - remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); - continue; - } - } - - writePendingWorldDeleteMap(remaining); - } catch (Throwable e) { - Iris.error("Failed to process queued startup world deletions."); - reportError(e); - e.printStackTrace(); - } - } - - private static LinkedHashMap loadPendingWorldDeleteMap() throws IOException { - LinkedHashMap queue = new LinkedHashMap<>(); - if (instance == null) { - return queue; - } - - File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE); - if (!queueFile.exists()) { - return queue; - } - - try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) { - String line; - while ((line = reader.readLine()) != null) { - String normalized = normalizeWorldName(line); - if (normalized == null) { - continue; - } - queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized); - } - } - - return queue; - } - - private static void writePendingWorldDeleteMap(Map queue) throws IOException { - if (instance == null) { - return; - } - - File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE); - if (queue.isEmpty()) { - if (queueFile.exists()) { - IO.delete(queueFile); - } - return; - } - - File parent = queueFile.getParentFile(); - if (parent != null && !parent.exists() && !parent.mkdirs()) { - throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath()); - } - - try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) { - for (String worldName : queue.values()) { - writer.println(worldName); - } - } - } - - @Nullable - private static String normalizeWorldName(String worldName) { - if (worldName == null) { - return null; - } - - String trimmed = worldName.trim(); - if (trimmed.isEmpty()) { - return null; - } - - return trimmed; + public BukkitWorldReconciler worldReconciler() { + return worldReconciler; } private void autoStartStudio() { @@ -1012,13 +679,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { public void onDisable() { teardownPapi(); if (IrisSafeguard.isForceShutdown()) return; - if (alreadyDrained.compareAndSet(false, true)) { - drainWorldGenerators("onDisable", 30L); - } - if (services != null) { - services.values().forEach(IrisService::onDisable); - } - IrisServices.clear(); + teardownRuntime("onDisable", 30L); if (BukkitPlatform.hasHud()) { BukkitPlatform.hudSlots().shutdown(); BukkitPlatform.hudLanes().shutdown(); @@ -1039,12 +700,54 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { @Override public void onPreUnload(ReloadAware.PreUnloadReason reason) { teardownPapi(); - if (!alreadyDrained.compareAndSet(false, true)) { + if (alreadyDrained.get()) { Iris.info("Pre-unload hook skipped; Iris already drained."); return; } Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds."); - drainWorldGenerators("pre-unload:" + reason, 45L); + drainOnce("pre-unload:" + reason, 45L); + } + + /** + * Drains the world generators exactly once. Serialized against the JVM shutdown hook so a + * second caller cannot rip the pools or services out from under an in-flight drain. + */ + private void drainOnce(String reason, long timeoutSeconds) { + synchronized (TEARDOWN_LOCK) { + if (alreadyDrained.compareAndSet(false, true)) { + drainWorldGenerators(reason, timeoutSeconds); + } + } + } + + /** + * Full teardown: generators, then services, then the shared pools and the service map. + * Both onDisable and the JVM shutdown hook route through here; whichever runs second is a no-op. + */ + private void teardownRuntime(String reason, long timeoutSeconds) { + synchronized (TEARDOWN_LOCK) { + if (alreadyDrained.compareAndSet(false, true)) { + drainWorldGenerators(reason, timeoutSeconds); + } + + if (services != null && servicesDisabled.compareAndSet(false, true)) { + for (IrisService service : services.values()) { + try { + service.onDisable(); + } catch (Throwable e) { + Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e); + } + } + } + + if (!sharedRuntimeClosed.compareAndSet(false, true)) { + return; + } + + J.attempt(MultiBurst.burst::close); + J.attempt(MultiBurst.ioBurst::close); + IrisServices.clear(); + } } private void drainWorldGenerators(String reason, long timeoutSeconds) { @@ -1164,61 +867,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { return IrisSafeguard.mode().tag(subTag); } - private void checkConfigHotload() { - if (configHotloadEngine == null) { - return; - } - - for (File file : configHotloadEngine.pollTouchedFiles()) { - configHotloadEngine.processFileChange(file, ignored -> { - IrisSettings.invalidate(); - IrisSettings.get(); - IrisLanguage.reload(); - return true; - }, ignored -> Iris.info("Hotloaded settings.json ")); - } - IrisLanguage.update(); - } - - private static boolean isSettingsFile(File file) { - if (file == null || settingsFile == null) { - return false; - } - return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile()); - } - - private static List knownSettingsFiles() { - if (settingsFile == null) { - return List.of(); - } - return List.of(settingsFile); - } - - private static String readSettingsContent(File file) { - if (file == null || !file.exists() || !file.isFile()) { - return null; - } - - try { - return IO.readAll(file); - } catch (Throwable ex) { - Iris.warn("Failed to read settings file %s: %s%s", - file.getAbsolutePath(), - ex.getClass().getSimpleName(), - ex.getMessage() == null ? "" : " - " + ex.getMessage()); - Iris.reportError(ex); - return null; - } - } - - private static String normalizeSettingsContent(String text) { - if (text == null) { - return null; - } - - return text.replace("\r\n", "\n").trim(); - } - private void tickQueue() { synchronized (Iris.syncJobs) { if (!Iris.syncJobs.hasNext()) { @@ -1256,117 +904,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { @Nullable @Override public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { - org.bukkit.generator.BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName); - if (stagedBiomeProvider != null) { - Iris.debug("Using staged runtime biome provider for " + worldName); - return stagedBiomeProvider; - } - Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id); - return super.getDefaultBiomeProvider(worldName, id); + return generatorResolver.resolveDefaultBiomeProvider(worldName, id, () -> super.getDefaultBiomeProvider(worldName, id)); } @Override public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { - ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName); - if (stagedGenerator != null) { - Iris.debug("Using staged runtime generator for " + worldName); - return stagedGenerator; - } - Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id); - if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType(); - Iris.debug("Generator ID: " + id + " requested by bukkit/plugin"); - - PackValidationResult validation = PackValidationRegistry.get(id); - if (validation != null && !validation.isLoadable()) { - Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':"); - for (String reason : validation.getBlockingErrors()) { - Iris.error(" - " + reason); - } - throw new BrokenPackException(id, validation.getBlockingErrors()); - } - - IrisDimension dim = loadDimension(worldName, id); - if (dim == null) { - throw new RuntimeException("Can't find dimension " + id + "!"); - } - - Iris.debug("Assuming IrisDimension: " + dim.getName()); - NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName); - - IrisWorld w = IrisWorld.builder() - .platformIdentity(worldKey.toString()) - .name(worldName) - .seed(1337) - .worldFolder(IrisWorldStorage.dimensionRoot(worldKey)) - .minHeight(dim.getMinHeight()) - .maxHeight(dim.getMaxHeight()) - .build(); - - Iris.debug("Generator Config: " + w.toString()); - - File ff = new File(w.worldFolder(), "iris/pack"); - File[] files = ff.listFiles(); - if (files == null || files.length == 0) - IO.delete(ff); - - if (!ff.exists()) { - ff.mkdirs(); - dim = service(StudioSVC.class).installIntoWorld(getSender(), dim, w.worldFolder()); - if (dim == null) { - throw new IllegalStateException("Failed to install dimension pack for " + id); - } - } - - return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey()); - } - - public static void validateAllPacks() { - File packsRoot = Iris.instance.getDataFolder("packs"); - File[] packDirs = packsRoot.listFiles(File::isDirectory); - if (packDirs == null || packDirs.length == 0) { - return; - } - PackValidationRegistry.clear(); - for (File packDir : packDirs) { - try { - PackValidationResult result = PackValidator.validate(packDir); - PackValidationRegistry.publish(result); - if (!result.isLoadable()) { - Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:"); - for (String reason : result.getBlockingErrors()) { - Iris.error(" - " + reason); - } - } else if (!result.getWarnings().isEmpty()) { - Iris.info("Pack '" + result.getPackName() + "' validated (" - + result.getWarnings().size() + " warning(s))."); - for (String warning : result.getWarnings()) { - Iris.warn(" [" + result.getPackName() + "] " + warning); - } - } else { - Iris.success("Pack '" + result.getPackName() + "' validated."); - } - } catch (Throwable e) { - Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e); - } - } - } - - @Nullable - public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) { - File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName)); - IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; - if (dimension == null) dimension = IrisData.loadAnyDimension(id, null); - if (dimension == null) { - Iris.warn("Unable to find dimension type " + id + " Looking for online packs..."); - Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); - dimension = IrisData.loadAnyDimension(id, null); - - if (dimension != null) { - Iris.info("Resolved missing dimension, proceeding."); - } - } - - return dimension; + return generatorResolver.resolveDefaultWorldGenerator(worldName, id); } public void splash() { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java new file mode 100644 index 000000000..04508489d --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/BukkitWorldReconciler.java @@ -0,0 +1,110 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.lifecycle.WorldLifecycleService; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.platform.bukkit.BukkitEnvironment; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KList; +import org.bukkit.NamespacedKey; +import org.bukkit.WorldCreator; +import org.bukkit.generator.ChunkGenerator; + +import java.util.function.Predicate; + +/** + * Loads Iris worlds that are staged in bukkit.yml but not yet present on the server. + */ +public final class BukkitWorldReconciler { + private final Iris plugin; + + public BukkitWorldReconciler(Iris plugin) { + this.plugin = plugin; + } + + public void checkForBukkitWorlds(Predicate filter) { + try { + KList deferredStartupWorlds = new KList<>(); + IrisWorlds.readBukkitWorlds().forEach((s, generator) -> { + try { + NamespacedKey worldKey = IrisWorldStorage.keyFromName(s); + if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return; + + Iris.info("Loading World: %s | Generator: %s", s, generator); + ChunkGenerator gen = plugin.getDefaultWorldGenerator(s, generator); + IrisDimension dim = IrisWorldGeneratorResolver.loadDimension(s, generator); + assert dim != null && gen != null; + + Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "..."); + WorldCreator c = WorldCreator.ofKey(worldKey) + .generator(gen) + .environment(BukkitEnvironment.from(dim.getEnvironment())); + Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s); + if (stagedSeed != null) { + c.seed(stagedSeed); + } + INMS.get().createWorld(c); + Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!"); + } catch (Throwable e) { + if (containsCreateWorldUnsupportedOperation(e)) { + if (J.isFolia()) { + if (!deferredStartupWorlds.contains(s)) { + deferredStartupWorlds.add(s); + } + return; + } + Iris.error("Failed to load world " + s + "!"); + Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase."); + Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml."); + Iris.reportError("Failed to load staged startup world \"" + s + "\".", e); + return; + } + Iris.reportError("Failed to load startup world \"" + s + "\".", e); + } + }); + if (!deferredStartupWorlds.isEmpty()) { + Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds)); + Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution()); + } + } catch (Throwable e) { + Iris.reportError("Failed while loading startup Iris worlds.", e); + } + } + + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { + Throwable cursor = throwable; + while (cursor != null) { + if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) { + for (StackTraceElement element : cursor.getStackTrace()) { + if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName()) + && "createWorld".equals(element.getMethodName())) { + return true; + } + } + } + cursor = cursor.getCause(); + } + return false; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java new file mode 100644 index 000000000..5815631b9 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java @@ -0,0 +1,173 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.lifecycle.WorldLifecycleStaging; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.iris.core.pack.PackValidator; +import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.platform.BukkitChunkGenerator; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.io.IO; +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.function.Supplier; + +/** + * Pack validation, dimension lookup, and the world generator / biome provider resolution that the + * Bukkit plugin entry points delegate to. + */ +public final class IrisWorldGeneratorResolver { + private final VolmitPlugin plugin; + + public IrisWorldGeneratorResolver(VolmitPlugin plugin) { + this.plugin = plugin; + } + + public void validateAllPacks() { + File packsRoot = plugin.getDataFolder("packs"); + File[] packDirs = packsRoot.listFiles(File::isDirectory); + if (packDirs == null || packDirs.length == 0) { + return; + } + PackValidationRegistry.clear(); + for (File packDir : packDirs) { + try { + PackValidationResult result = PackValidator.validate(packDir); + PackValidationRegistry.publish(result); + if (!result.isLoadable()) { + Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:"); + for (String reason : result.getBlockingErrors()) { + Iris.error(" - " + reason); + } + } else if (!result.getWarnings().isEmpty()) { + Iris.info("Pack '" + result.getPackName() + "' validated (" + + result.getWarnings().size() + " warning(s))."); + for (String warning : result.getWarnings()) { + Iris.warn(" [" + result.getPackName() + "] " + warning); + } + } else { + Iris.success("Pack '" + result.getPackName() + "' validated."); + } + } catch (Throwable e) { + Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e); + } + } + } + + @Nullable + public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) { + File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName)); + IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null; + if (dimension == null) dimension = IrisData.loadAnyDimension(id, null); + if (dimension == null) { + Iris.warn("Unable to find dimension type " + id + " Looking for online packs..."); + Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); + dimension = IrisData.loadAnyDimension(id, null); + + if (dimension != null) { + Iris.info("Resolved missing dimension, proceeding."); + } + } + + return dimension; + } + + /** + * Resolves the biome provider for a world, falling back to the supplied Bukkit default when + * Iris has nothing staged. + */ + @Nullable + public BiomeProvider resolveDefaultBiomeProvider(String worldName, @Nullable String id, Supplier fallback) { + BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName); + if (stagedBiomeProvider != null) { + Iris.debug("Using staged runtime biome provider for " + worldName); + return stagedBiomeProvider; + } + Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id); + return fallback.get(); + } + + public ChunkGenerator resolveDefaultWorldGenerator(String worldName, String id) { + ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName); + if (stagedGenerator != null) { + Iris.debug("Using staged runtime generator for " + worldName); + return stagedGenerator; + } + Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id); + if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType(); + Iris.debug("Generator ID: " + id + " requested by bukkit/plugin"); + + PackValidationResult validation = PackValidationRegistry.get(id); + if (validation != null && !validation.isLoadable()) { + Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':"); + for (String reason : validation.getBlockingErrors()) { + Iris.error(" - " + reason); + } + throw new BrokenPackException(id, validation.getBlockingErrors()); + } + + IrisDimension dim = loadDimension(worldName, id); + if (dim == null) { + throw new RuntimeException("Can't find dimension " + id + "!"); + } + + Iris.debug("Assuming IrisDimension: " + dim.getName()); + NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName); + + IrisWorld w = IrisWorld.builder() + .platformIdentity(worldKey.toString()) + .name(worldName) + .seed(1337) + .worldFolder(IrisWorldStorage.dimensionRoot(worldKey)) + .minHeight(dim.getMinHeight()) + .maxHeight(dim.getMaxHeight()) + .build(); + + Iris.debug("Generator Config: " + w.toString()); + + File ff = new File(w.worldFolder(), "iris/pack"); + File[] files = ff.listFiles(); + if (files == null || files.length == 0) + IO.delete(ff); + + if (!ff.exists()) { + ff.mkdirs(); + dim = Iris.service(StudioSVC.class).installIntoWorld(Iris.getSender(), dim, w.worldFolder()); + if (dim == null) { + throw new IllegalStateException("Failed to install dimension pack for " + id); + } + } + + return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java new file mode 100644 index 000000000..cd2bd0583 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/PendingWorldDeleteQueue.java @@ -0,0 +1,228 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; +import art.arcane.iris.core.tools.IrisCreator; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.iris.util.common.plugin.VolmitPlugin; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.io.IO; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Persistent queue of world folders that must be deleted on the next startup, plus the startup + * drain that actually removes them. + */ +public final class PendingWorldDeleteQueue { + private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt"; + + private final VolmitPlugin plugin; + + public PendingWorldDeleteQueue(VolmitPlugin plugin) { + this.plugin = plugin; + } + + public synchronized int queueWorldDeletionOnStartup(Collection worldNames) throws IOException { + if (worldNames == null || worldNames.isEmpty()) { + return 0; + } + + LinkedHashMap queue = loadPendingWorldDeleteMap(); + int before = queue.size(); + + for (String worldName : worldNames) { + String normalized = normalizeWorldName(worldName); + if (normalized == null) { + continue; + } + queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized); + } + + if (queue.size() != before) { + writePendingWorldDeleteMap(queue); + } + + return queue.size() - before; + } + + public void processPendingStartupWorldDeletes() { + try { + try { + int unregistered = IrisCreator.removeTransientStudioWorldsFromBukkitYml(); + if (unregistered > 0) { + Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup."); + } + } catch (Throwable e) { + Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e); + } + + LinkedHashMap queue = loadPendingWorldDeleteMap(); + for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) { + queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld); + } + if (queue.isEmpty()) { + return; + } + + LinkedHashMap remaining = new LinkedHashMap<>(); + for (String worldName : queue.values()) { + if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) { + Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name."); + continue; + } + + NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName); + World loaded = WorldIdentity.resolve(worldKey).orElse(null); + if (loaded != null) { + if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) { + try { + PlatformChunkGenerator generator = IrisToolbelt.access(loaded); + if (generator != null) { + generator.close(); + } + IrisToolbelt.evacuate(loaded); + Bukkit.unloadWorld(loaded, false); + Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion."); + } catch (Throwable e) { + Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e); + } + + if (WorldIdentity.resolve(worldKey).isPresent()) { + Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup."); + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + continue; + } + } else { + Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded."); + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + continue; + } + } + + boolean foundAny = false; + boolean deletedAll = true; + for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) { + File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName); + if (!worldFolder.exists()) { + continue; + } + + foundAny = true; + IO.delete(worldFolder); + if (worldFolder.exists()) { + deletedAll = false; + Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup."); + } else { + Iris.info("Deleted queued world folder \"" + familyWorldName + "\"."); + } + } + + if (!foundAny) { + Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing)."); + continue; + } + + if (!deletedAll) { + remaining.put(worldName.toLowerCase(Locale.ROOT), worldName); + continue; + } + } + + writePendingWorldDeleteMap(remaining); + } catch (Throwable e) { + Iris.error("Failed to process queued startup world deletions."); + Iris.reportError(e); + e.printStackTrace(); + } + } + + private LinkedHashMap loadPendingWorldDeleteMap() throws IOException { + LinkedHashMap queue = new LinkedHashMap<>(); + File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE); + if (!queueFile.exists()) { + return queue; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) { + String line; + while ((line = reader.readLine()) != null) { + String normalized = normalizeWorldName(line); + if (normalized == null) { + continue; + } + queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized); + } + } + + return queue; + } + + private void writePendingWorldDeleteMap(Map queue) throws IOException { + File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE); + if (queue.isEmpty()) { + if (queueFile.exists()) { + IO.delete(queueFile); + } + return; + } + + File parent = queueFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath()); + } + + try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) { + for (String worldName : queue.values()) { + writer.println(worldName); + } + } + } + + @Nullable + private static String normalizeWorldName(String worldName) { + if (worldName == null) { + return null; + } + + String trimmed = worldName.trim(); + if (trimmed.isEmpty()) { + return null; + } + + return trimmed; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java new file mode 100644 index 000000000..d3f264ceb --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/SettingsHotloadWatch.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; +import art.arcane.volmlib.util.io.IO; + +import java.io.File; +import java.util.List; + +/** + * Identity and hotload handling for settings.json. Supplies the predicates the + * {@link ConfigHotloadEngine} is built from and drains the touched-file queue. + */ +public final class SettingsHotloadWatch { + private final File settingsFile; + + public SettingsHotloadWatch(File settingsFile) { + this.settingsFile = settingsFile; + } + + public File settingsFile() { + return settingsFile; + } + + public void checkConfigHotload(ConfigHotloadEngine engine) { + if (engine == null) { + return; + } + + for (File file : engine.pollTouchedFiles()) { + engine.processFileChange(file, ignored -> { + IrisSettings.invalidate(); + IrisSettings.get(); + IrisLanguage.reload(); + return true; + }, ignored -> Iris.info("Hotloaded settings.json ")); + } + IrisLanguage.update(); + } + + public boolean isSettingsFile(File file) { + if (file == null || settingsFile == null) { + return false; + } + return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile()); + } + + public List knownSettingsFiles() { + if (settingsFile == null) { + return List.of(); + } + return List.of(settingsFile); + } + + public String readSettingsContent(File file) { + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + + try { + return IO.readAll(file); + } catch (Throwable ex) { + Iris.warn("Failed to read settings file %s: %s%s", + file.getAbsolutePath(), + ex.getClass().getSimpleName(), + ex.getMessage() == null ? "" : " - " + ex.getMessage()); + Iris.reportError(ex); + return null; + } + } + + public String normalizeSettingsContent(String text) { + if (text == null) { + return null; + } + + return text.replace("\r\n", "\n").trim(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java index f72baca17..f278dfa45 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -51,6 +51,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import org.bukkit.Bukkit; import org.bukkit.World; +import org.bukkit.entity.Player; import java.io.BufferedInputStream; import java.io.File; @@ -283,7 +284,7 @@ public class CommandDeveloper implements DirectorExecutor { } - @Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true) + @Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER) public void deleteChunk( @Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0") int radius @@ -293,25 +294,34 @@ public class CommandDeveloper implements DirectorExecutor { return; } - World world = player().getWorld(); + Player player = player(); + VolmitSender commandSender = sender(); + World world = player.getWorld(); if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD)); return; } PlatformChunkGenerator access = IrisToolbelt.access(world); if (access == null || access.getEngine() == null) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL)); return; } - int centerX = player().getLocation().getBlockX() >> 4; - int centerZ = player().getLocation().getBlockZ() >> 4; + Engine engine = access.getEngine(); int chunks = (radius * 2 + 1) * (radius * 2 + 1); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); + // The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself. + if (!J.runEntity(player, () -> { + int centerX = player.getLocation().getBlockX() >> 4; + int centerZ = player.getLocation().getBlockZ() >> 4; - new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start(); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); + + new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start(); + })) { + Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + "."); + } } @Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"}) @@ -332,7 +342,7 @@ public class CommandDeveloper implements DirectorExecutor { // --- Regen --- - @Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER, sync = true) + @Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER) public void regen( @Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5") int radius @@ -342,29 +352,37 @@ public class CommandDeveloper implements DirectorExecutor { return; } - World world = player().getWorld(); + Player player = player(); + VolmitSender commandSender = sender(); + World world = player.getWorld(); if (!IrisToolbelt.isIrisWorld(world)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN)); return; } Engine engine = IrisToolbelt.access(world).getEngine(); if (engine == null) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST)); return; } - int centerX = player().getLocation().getBlockX() >> 4; - int centerZ = player().getLocation().getBlockZ() >> 4; int chunks = (radius * 2 + 1) * (radius * 2 + 1); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); - Iris.info("Regen run start: world=" + world.getName() - + " center=" + centerX + "," + centerZ - + " radius=" + radius - + " chunks=" + chunks); + // The player position must be read on the thread owning the player; the regenerator hops per chunk itself. + if (!J.runEntity(player, () -> { + int centerX = player.getLocation().getBlockX() >> 4; + int centerZ = player.getLocation().getBlockZ() >> 4; - new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start(); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ))); + Iris.info("Regen run start: world=" + world.getName() + + " center=" + centerX + "," + centerZ + + " radius=" + radius + + " chunks=" + chunks); + + new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start(); + })) { + Iris.warn("Could not schedule regen on the thread owning " + player.getName() + "."); + } } @Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java index dea27eb5f..5b46a2fca 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -316,19 +316,6 @@ public class CommandIris implements DirectorExecutor { sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion()))); } - /* - /todo - @Director(description = "Benchmark a pack", descriptionKey = "iris.director.commandiris.director.benchmark_pack", origin = DirectorOrigin.CONSOLE) - public void packbenchmark( - @Param(description = "Dimension to benchmark", descriptionKey = "iris.director.commandiris.param.dimension_benchmark") - IrisDimension type - ) throws InterruptedException { - - BenchDimension = type.getLoadKey(); - - IrisPackBenchmarking.runBenchmark(); - } */ - @Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER) public void height() { if (sender().isPlayer()) { @@ -588,7 +575,7 @@ public class CommandIris implements DirectorExecutor { return; } - Iris.instance.checkForBukkitWorlds(logicalWorldName::equals); + Iris.instance.worldReconciler().checkForBukkitWorlds(logicalWorldName::equals); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY, MessageArgument.untrusted("logicalWorldName", logicalWorldName))); } @Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java index 5ec23f2af..2d0ad45e4 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -428,8 +428,13 @@ public class CommandObject implements DirectorExecutor { @Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d") public void dust() { - player().getInventory().addItem(WandSVC.createDust()); - sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f); + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + player.getInventory().addItem(WandSVC.createDust()); + commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f); + }); } @Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-") @@ -437,28 +442,33 @@ public class CommandObject implements DirectorExecutor { @Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1") int amount ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND)); - return; - } + VolmitSender commandSender = sender(); + Player player = player(); + + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND)); + return; + } - Location[] b = WandSVC.getCuboid(player()); - if (b == null || b[0] == null || b[1] == null) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED)); - return; - } - Location a1 = b[0].clone(); - Location a2 = b[1].clone(); - Cuboid cursor = new Cuboid(a1, a2); - Direction d = Direction.closest(player().getLocation().getDirection()).reverse(); - assert d != null; - cursor = cursor.expand(d.f(), -amount); - b[0] = cursor.getLowerNE(); - b[1] = cursor.getUpperSW(); - player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); - player().updateInventory(); - sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + Location[] b = WandSVC.getCuboid(player); + if (b == null || b[0] == null || b[1] == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED)); + return; + } + Location a1 = b[0].clone(); + Location a2 = b[1].clone(); + Cuboid cursor = new Cuboid(a1, a2); + Direction d = Direction.closest(player.getLocation().getDirection()).reverse(); + assert d != null; + cursor = cursor.expand(d.f(), -amount); + b[0] = cursor.getLowerNE(); + b[1] = cursor.getUpperSW(); + player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); + player.updateInventory(); + commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + }); } @Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1") @@ -466,25 +476,30 @@ public class CommandObject implements DirectorExecutor { @Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true") boolean here ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND)); - return; - } + VolmitSender commandSender = sender(); + Player player = player(); - if (WandSVC.isHoldingWand(player())) { - Location[] g = WandSVC.getCuboid(player()); - - if (g == null) { + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND)); return; } - if (!here) { - // TODO: WARNING HEIGHT - g[1] = player().getTargetBlock(null, 256).getLocation().clone(); - } else { - g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0); + + if (WandSVC.isHoldingWand(player)) { + Location[] g = WandSVC.getCuboid(player); + + if (g == null) { + return; + } + if (!here) { + // TODO: WARNING HEIGHT + g[1] = player.getTargetBlock(null, 256).getLocation().clone(); + } else { + g[1] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0); + } + player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1])); } - player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1])); - } + }); } @Director(description = "Set point 2 to look", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2") @@ -492,26 +507,31 @@ public class CommandObject implements DirectorExecutor { @Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true") boolean here ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2)); - return; - } + VolmitSender commandSender = sender(); + Player player = player(); - if (WandSVC.isHoldingIrisWand(player())) { - Location[] g = WandSVC.getCuboid(player()); - - if (g == null) { + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2)); return; } - if (!here) { - // TODO: WARNING HEIGHT - g[0] = player().getTargetBlock(null, 256).getLocation().clone(); - } else { - g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0); + if (WandSVC.isHoldingIrisWand(player)) { + Location[] g = WandSVC.getCuboid(player); + + if (g == null) { + return; + } + + if (!here) { + // TODO: WARNING HEIGHT + g[0] = player.getTargetBlock(null, 256).getLocation().clone(); + } else { + g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0); + } + player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1])); } - player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1])); - } + }); } @Director(description = "Paste an object", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true) @@ -540,8 +560,10 @@ public class CommandObject implements DirectorExecutor { IrisObjectPlacement placement = new IrisObjectPlacement(); placement.setRotation(IrisObjectRotation.of(0, rotate, 0)); - ItemStack wand = player().getInventory().getItemInMainHand(); - Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0); + VolmitSender commandSender = sender(); + Player player = player(); + ItemStack wand = player.getInventory().getItemInMainHand(); + Location block = player.getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0); Map futureChanges = new HashMap<>(); @@ -549,30 +571,50 @@ public class CommandObject implements DirectorExecutor { o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC); } - o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null); + // Block writes must run on the thread owning the target chunk; the undo log stays global. + final IrisObject placed = o; + if (!J.runAt(block, () -> { + placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null); + J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges)); - Iris.service(ObjectSVC.class).addChanges(futureChanges); - - if (edit) { - Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ()); - ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1, - o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0))); - if (WandSVC.isWand(wand)) { - wand = newWand; - player().getInventory().setItemInMainHand(wand); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey()))); - } else { - int slot = WandSVC.findWand(player().getInventory()); - if (slot == -1) { - player().getInventory().addItem(newWand); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey()))); - } else { - player().getInventory().setItem(slot, newWand); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", o.getLoadKey()))); - } + if (!edit) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object))); + return; } - } else { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object))); + + onPlayerThread(player, () -> { + Vector center = new Vector(placed.getCenter().getX(), placed.getCenter().getY(), placed.getCenter().getZ()); + ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(placed.getW() - 1, + placed.getH() + center.getY() - 1, placed.getD() - 1), block.clone().subtract(center.clone().setY(0))); + if (WandSVC.isWand(wand)) { + player.getInventory().setItemInMainHand(newWand); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey()))); + } else { + int slot = WandSVC.findWand(player.getInventory()); + if (slot == -1) { + player.getInventory().addItem(newWand); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey()))); + } else { + player.getInventory().setItem(slot, newWand); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey()))); + } + } + }); + })) { + Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + "."); + } + } + + /** + * Runs the body on the thread owning the player, reporting when the hop cannot be scheduled. + */ + private void onPlayerThread(Player player, Runnable body) { + if (player == null) { + return; + } + + if (!J.runEntity(player, body)) { + Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + "."); } } @@ -616,30 +658,35 @@ public class CommandObject implements DirectorExecutor { @Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1") int amount ) { - if (!WandSVC.isHoldingWand(player())) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2)); - return; - } + VolmitSender commandSender = sender(); + Player player = player(); - Location[] b = WandSVC.getCuboid(player()); - if (b == null || b[0] == null || b[1] == null) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2)); - return; - } - Location a1 = b[0].clone(); - Location a2 = b[1].clone(); - Direction d = Direction.closest(player().getLocation().getDirection()).reverse(); - if (d == null) { - return; // HOW DID THIS HAPPEN - } - a1.add(d.toVector().multiply(amount)); - a2.add(d.toVector().multiply(amount)); - Cuboid cursor = new Cuboid(a1, a2); - b[0] = cursor.getLowerNE(); - b[1] = cursor.getUpperSW(); - player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); - player().updateInventory(); - sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + onPlayerThread(player, () -> { + if (!WandSVC.isHoldingWand(player)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2)); + return; + } + + Location[] b = WandSVC.getCuboid(player); + if (b == null || b[0] == null || b[1] == null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2)); + return; + } + Location a1 = b[0].clone(); + Location a2 = b[1].clone(); + Direction d = Direction.closest(player.getLocation().getDirection()).reverse(); + if (d == null) { + return; // HOW DID THIS HAPPEN + } + a1.add(d.toVector().multiply(amount)); + a2.add(d.toVector().multiply(amount)); + Cuboid cursor = new Cuboid(a1, a2); + b[0] = cursor.getLowerNE(); + b[1] = cursor.getUpperSW(); + player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1])); + player.updateInventory(); + commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f); + }); } @Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u") @@ -655,20 +702,25 @@ public class CommandObject implements DirectorExecutor { @Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER) public void we() { + VolmitSender commandSender = sender(); + Player player = player(); + if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW)); return; } - Cuboid locs = WorldEditLink.getSelection(sender().player()); + Cuboid locs = WorldEditLink.getSelection(player); if (locs == null) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD)); return; } - sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW())); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT)); + onPlayerThread(player, () -> { + player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW())); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT)); + }); } @Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 7c28a968c..253ab31b3 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -26,6 +26,7 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI; import art.arcane.iris.core.gui.VisionGUI; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.core.service.BoardSVC; import art.arcane.iris.core.service.StudioSVC; @@ -104,6 +105,8 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -114,6 +117,7 @@ import art.arcane.volmlib.util.localization.MessageArgument; import art.arcane.iris.core.localization.BukkitCommandMessagesExtended; @Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands") public class CommandStudio implements DirectorExecutor { + private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000; private CommandEdit edit; //private CommandDeepSearch deepSearch; @@ -662,10 +666,18 @@ public class CommandStudio implements DirectorExecutor { @Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true) Vector location ) { + VolmitSender commandSender = sender(); + Engine spawnEngine = engine(); + if (!IrisToolbelt.isIrisWorld(player().getWorld())) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN)); + } + + // Entity creation must run on the thread owning the destination chunk. + Location at = new Location(world(), location.getX(), location.getY(), location.getZ()); + if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) { + Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + "."); } - entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ())); } @Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true) @@ -697,7 +709,7 @@ public class CommandStudio implements DirectorExecutor { IrisDimension dimension ) { sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName()))); - if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) { + if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) { sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName()))); } else { sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName()))); @@ -718,20 +730,46 @@ public class CommandStudio implements DirectorExecutor { return; } KList chunks = new KList<>(); - int bx = player().getLocation().getChunk().getX(); - int bz = player().getLocation().getChunk().getZ(); + Player reporter = player(); + CountDownLatch gathered = new CountDownLatch(1); - try { - Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation(); + // The raycast and the chunk loads need the thread owning the player; the report itself stays off it. + boolean scheduled = J.runEntity(reporter, () -> { + try { + int bx = reporter.getLocation().getChunk().getX(); + int bz = reporter.getLocation().getChunk().getZ(); - int cx = l.getChunk().getX(); - int cz = l.getChunk().getZ(); - new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain(); - } catch (Throwable e) { - Iris.reportError(e); + try { + Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation(); + + int cx = l.getChunk().getX(); + int cz = l.getChunk().getZ(); + new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain(); + } catch (Throwable e) { + Iris.reportError(e); + } + + new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain(); + } finally { + gathered.countDown(); + } + }); + + if (!scheduled) { + Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + "."); + return; + } + + try { + if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + "."); + return; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; } - new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain(); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size()))); try { File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt"); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java index 43c9a044f..81d8d1fb7 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java @@ -25,10 +25,13 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.edit.BlockSignal; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.platform.EngineBukkitOps; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -38,8 +41,10 @@ import org.bukkit.Chunk; import org.bukkit.FluidCollisionMode; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicInteger; @@ -97,83 +102,115 @@ public class CommandWhat implements DirectorExecutor { @Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER) public void region() { - try { - Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4); - IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName()))); + VolmitSender commandSender = sender(); + Player player = player(); + World world = world(); + Engine engine = engine(); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY)); - } + // Chunk access must happen on the thread owning the player's chunk. + onPlayerThread(player, () -> { + try { + Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4); + IrisRegion r = EngineBukkitOps.getRegion(engine, chunk); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName()))); + + } catch (Throwable e) { + Iris.reportError(e); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY)); + } + }); } @Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER) public void block() { - BlockData bd; - try { - bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData(); - } catch (NullPointerException e) { - Iris.reportError(e); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY)); - bd = null; - } + VolmitSender commandSender = sender(); + Player player = player(); - if (bd != null) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name()))); - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true)))); - - if (BukkitBlockResolution.isStorage(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE)); + // The raycast reads blocks, so it has to run on the thread owning the player. + onPlayerThread(player, () -> { + BlockData bd; + try { + bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData(); + } catch (NullPointerException e) { + Iris.reportError(e); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY)); + bd = null; } - if (BukkitBlockResolution.isLit(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE)); - } + if (bd != null) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name()))); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true)))); - if (BukkitBlockResolution.isFoliage(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK)); - } + if (BukkitBlockResolution.isStorage(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE)); + } - if (BukkitBlockResolution.isDecorant(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK)); - } + if (BukkitBlockResolution.isLit(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE)); + } - if (BukkitBlockResolution.isFluid(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK)); - } + if (BukkitBlockResolution.isFoliage(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK)); + } - if (BukkitBlockResolution.isFoliagePlantable(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK)); - } + if (BukkitBlockResolution.isDecorant(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK)); + } - if (BukkitBlockResolution.isSolid(bd)) { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK)); + if (BukkitBlockResolution.isFluid(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK)); + } + + if (BukkitBlockResolution.isFoliagePlantable(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK)); + } + + if (BukkitBlockResolution.isSolid(bd)) { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK)); + } } - } + }); } @Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER) public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) { - Chunk c = player().getLocation().getChunk(); + VolmitSender commandSender = sender(); + Player player = player(); - if (IrisToolbelt.isIrisWorld(c.getWorld())) { - int m = 1; - AtomicInteger v = new AtomicInteger(0); + // Chunk lookup plus the block signals both need the thread owning the player's chunk. + onPlayerThread(player, () -> { + Chunk c = player.getLocation().getChunk(); - for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) { - for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) { - IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker)) - .convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> { - BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100); - v.incrementAndGet(); - }); + if (IrisToolbelt.isIrisWorld(c.getWorld())) { + AtomicInteger v = new AtomicInteger(0); + + for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) { + for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) { + IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker)) + .convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> { + BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100); + v.incrementAndGet(); + }); + } } - } - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker))); - } else { - sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2)); + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker))); + } else { + commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2)); + } + }); + } + + /** + * Runs the body on the thread owning the player, reporting when the hop cannot be scheduled. + */ + private void onPlayerThread(Player player, Runnable body) { + if (player == null) { + return; + } + + if (!J.runEntity(player, body)) { + Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + "."); } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java index 08fea134c..e0a7510a5 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/edit/BukkitBlockEditor.java @@ -23,21 +23,24 @@ import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; -@SuppressWarnings("ClassCanBeRecord") public class BukkitBlockEditor implements BlockEditor { private final World world; + private volatile long last; public BukkitBlockEditor(World world) { this.world = world; + this.last = M.ms(); } @Override public void set(int x, int y, int z, BlockData d) { + touch(); world.getBlockAt(x, y, z).setBlockData(d, false); } @Override public BlockData get(int x, int y, int z) { + touch(); return world.getBlockAt(x, y, z).getBlockData(); } @@ -48,11 +51,12 @@ public class BukkitBlockEditor implements BlockEditor { @Override public long last() { - return M.ms(); + return last; } @Override public void setBiome(int x, int z, Biome b) { + touch(); int minHeight = world.getMinHeight(); int maxHeight = world.getMaxHeight(); for (int y = minHeight; y < maxHeight; y++) { @@ -62,16 +66,23 @@ public class BukkitBlockEditor implements BlockEditor { @Override public void setBiome(int x, int y, int z, Biome b) { + touch(); world.setBiome(x, y, z, b); } @Override public Biome getBiome(int x, int y, int z) { + touch(); return world.getBiome(x, y, z); } @Override public Biome getBiome(int x, int z) { + touch(); return world.getBiome(x, world.getMinHeight(), z); } + + private void touch() { + last = M.ms(); + } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java index 126f4bce4..6202bb0b6 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java @@ -18,6 +18,7 @@ package art.arcane.iris.core.gui; +import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitWorldBinding; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.Engine; @@ -26,72 +27,141 @@ import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.format.Form; import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH; public final class BukkitVisionOverlay implements GuiOverlay { private final Engine engine; + private final AtomicBoolean playerRefreshQueued = new AtomicBoolean(); + private volatile List playerMarkers = List.of(); public BukkitVisionOverlay(Engine engine) { this.engine = engine; } + /** + * Called from the AWT event thread, so it may only hand back the last snapshot + * built by a server thread. + */ @Override public List players() { - IrisWorld world = engine.getWorld(); - List markers = new ArrayList<>(); - for (Player player : BukkitWorldBinding.players(world)) { - markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ())); + queuePlayerRefresh(); + return playerMarkers; + } + + private void queuePlayerRefresh() { + if (!playerRefreshQueued.compareAndSet(false, true)) { + return; + } + + boolean scheduled = J.runGlobal(() -> { + try { + List markers = new ArrayList<>(); + for (Player player : BukkitWorldBinding.players(engine.getWorld())) { + Location at = player.getLocation(); + markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ())); + } + playerMarkers = List.copyOf(markers); + } finally { + playerRefreshQueued.set(false); + } + }); + + if (!scheduled) { + playerRefreshQueued.set(false); } - return markers; } @Override public void requestEntities(Consumer> sink) { - J.s(() -> { - IrisWorld world = engine.getWorld(); - List markers = new ArrayList<>(); - for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) { - if (entity instanceof Player) { - continue; - } - String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")); - double maxHealth = 0; - try { - maxHealth = entity.getAttribute(MAX_HEALTH).getValue(); - } catch (Throwable ignored) { - } - markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(), - entity.getHealth(), maxHealth)); + J.runGlobal(() -> { + IrisWorld target = engine.getWorld(); + World world = BukkitWorldBinding.world(target); + if (world == null) { + sink.accept(List.of()); + return; + } + + List living = new ArrayList<>(); + for (LivingEntity entity : BukkitWorldBinding.entities(target, LivingEntity.class)) { + if (!(entity instanceof Player)) { + living.add(entity); + } + } + + if (living.isEmpty()) { + sink.accept(List.of()); + return; + } + + List collected = Collections.synchronizedList(new ArrayList<>(living.size())); + AtomicInteger pending = new AtomicInteger(living.size()); + Runnable complete = () -> { + if (pending.decrementAndGet() == 0) { + sink.accept(List.copyOf(collected)); + } + }; + + for (LivingEntity entity : living) { + Location at = entity.getLocation(); + Runnable read = () -> { + try { + collected.add(marker(entity, at)); + } catch (Throwable ignored) { + } finally { + complete.run(); + } + }; + + if (!J.runRegion(world, at.getBlockX() >> 4, at.getBlockZ() >> 4, read)) { + complete.run(); + } } - sink.accept(markers); }); } + private GuiMarker marker(LivingEntity entity, Location at) { + String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")); + double maxHealth = 0; + try { + maxHealth = entity.getAttribute(MAX_HEALTH).getValue(); + } catch (Throwable ignored) { + } + return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth); + } + @Override public void teleport(double worldX, double worldZ) { - IrisWorld world = engine.getWorld(); - if (!world.hasPlatformWorld()) { + IrisWorld target = engine.getWorld(); + if (!target.hasPlatformWorld()) { return; } - J.s(() -> { - List players = BukkitWorldBinding.players(world); + J.runGlobal(() -> { + List players = BukkitWorldBinding.players(target); if (players.isEmpty()) { return; } Player player = players.get(0); + World world = player.getWorld(); int xx = (int) worldX; int zz = (int) worldZ; - int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1; - player.teleport(new Location(player.getWorld(), xx, yy, zz)); + J.runRegion(world, xx >> 4, zz >> 4, () -> { + int yy = world.getHighestBlockYAt(xx, zz) + 1; + Location destination = new Location(world, xx, yy, zz); + J.runEntity(player, () -> BukkitPlatform.teleportAsync(player, destination)); + }); }); } 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 4b4d8adfc..a9301e305 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 @@ -24,6 +24,7 @@ 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.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; import art.arcane.iris.core.service.IrisApiEventSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.WorldMaintenance; @@ -41,7 +42,7 @@ import org.bukkit.World; public final class BukkitEnginePlatformHooks implements EnginePlatformHooks { @Override public void refreshWorkspace(Engine engine) { - new IrisProject(engine.getData().getDataFolder()).updateWorkspace(); + new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace(); } @Override diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java new file mode 100644 index 000000000..6daebfb7c --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/FellingRun.java @@ -0,0 +1,45 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.TreeFellerRunHooks; +import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate; +import art.arcane.iris.core.service.TreeFellerModel.TreeClaim; +import art.arcane.iris.core.service.TreeFellerModel.TreeMember; +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +final class FellingRun { + final TreeClaim claim; + final TreeCandidate candidate; + final int preservationChance; + final TreeFellerRunHooks runHooks; + final int heldSlot; + final TreeFellerPresentation presentation; + final AtomicBoolean finished = new AtomicBoolean(); + final AtomicInteger cursor = new AtomicInteger(); + final AtomicInteger processed = new AtomicInteger(); + volatile int blocksPerPulse = 1; + volatile int effectStride = 1; + volatile ItemStack expectedTool; + volatile List work = List.of(); + + FellingRun( + TreeClaim claim, + TreeCandidate candidate, + int preservationChance, + TreeFellerRunHooks runHooks, + int heldSlot, + Location fallbackLocation + ) { + this.claim = claim; + this.candidate = candidate; + this.preservationChance = preservationChance; + this.runHooks = runHooks; + this.heldSlot = heldSlot; + this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation); + this.expectedTool = candidate.tool().clone(); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index dfcbbe035..3c0960193 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -279,6 +279,11 @@ public final class IrisEngineSVC implements IrisService { } closing.completion().complete(null); } else { + // A failed close must still stop conflicting with future registrations, + // otherwise the world never regains its maintenance task after a reload. + synchronized (registrationLock) { + closingGenerators.remove(closing); + } closing.completion().completeExceptionally(failure); } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java new file mode 100644 index 000000000..a5c39a796 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/RoutedBlockBreakEvent.java @@ -0,0 +1,22 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.tree.BlockDropRouter; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; + +final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter { + private final TreeFellerSVC service; + private final FellingRun run; + + RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) { + super(block, player); + this.run = run; + this.service = service; + } + + @Override + public boolean routeDrop(Object drop) { + return service.isServiceEnabled() && run.presentation.routeDrop(drop); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java new file mode 100644 index 000000000..180c51b97 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerModel.java @@ -0,0 +1,81 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.TreeFellerAccess; +import art.arcane.iris.api.tree.TreeFellerRunHooks; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.UUID; + +final class TreeFellerModel { + private TreeFellerModel() { + } + + record PendingFell( + TreeCandidate candidate, + int preservationChance, + TreeFellerRunHooks runHooks + ) { + PendingFell withAccess(TreeFellerAccess access) { + return new PendingFell(candidate.withAccess(access), preservationChance, runHooks); + } + } + + record TreeCandidate( + TreeContext context, + World world, + Player player, + ItemStack tool, + TreeFellerAccess access, + TreeMarkerTraversal.Position trigger + ) { + TreeCandidate withAccess(TreeFellerAccess access) { + return new TreeCandidate(context, world, player, tool, access, trigger); + } + } + + record TreeContext( + Engine engine, + String marker, + TreeBlockMaterial expectedMaterial, + int minimumY, + int maximumY + ) { + } + + record TreeClaim(UUID worldId, String marker) { + } + + record ProvenanceSnapshot( + Engine engine, + World world, + int minimumY, + TreeMarkerTraversal.Position position, + String marker, + TreeBlockMaterial material + ) { + } + + record ChunkPosition(int x, int z) { + } + + record TreeMember( + TreeMarkerTraversal.Position position, + boolean log, + TreeBlockMaterial expectedMaterial, + int erosionOrder + ) { + } + + record DamageReservation( + ItemStack toolForDrops, + boolean charged, + boolean broke, + boolean logCostReserved + ) { + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java index 1051aac7a..3d2eb0a8e 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellerSVC.java @@ -5,27 +5,22 @@ import art.arcane.iris.api.tree.TreeFellerAccess; import art.arcane.iris.api.tree.TreeFellerOptions; import art.arcane.iris.api.tree.TreeFellerRunHooks; import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.core.service.tree.BlockDropRouter; +import art.arcane.iris.core.service.TreeFellerModel.PendingFell; +import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot; +import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate; +import art.arcane.iris.core.service.TreeFellerModel.TreeClaim; import art.arcane.iris.core.service.tree.TreeDefinitionIndex; -import art.arcane.iris.core.service.tree.TreeMarkerTraversal; -import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.framework.StructurePlacementMarker; -import art.arcane.iris.engine.framework.TreeBlockMaterial; -import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.iris.util.common.scheduling.J; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; -import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -36,40 +31,31 @@ import org.bukkit.event.player.PlayerItemHeldEvent; import org.bukkit.event.player.PlayerSwapHandItemsEvent; import org.bukkit.event.player.PlayerToggleSneakEvent; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.plugin.ServicePriority; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; public class TreeFellerSVC implements IrisService, IrisTreeFellerService { private static final String PERMISSION = "iris.treefeller"; private final AtomicBoolean serviceEnabled = new AtomicBoolean(); private final Map pending = Collections.synchronizedMap(new IdentityHashMap<>()); - private final Set managedEvents = Collections.synchronizedSet( + final Set managedEvents = Collections.synchronizedSet( Collections.newSetFromMap(new IdentityHashMap<>()) ); - private final Set activeClaims = ConcurrentHashMap.newKeySet(); - private final Map> activeRuns = new ConcurrentHashMap<>(); + final Set activeClaims = ConcurrentHashMap.newKeySet(); + final Map> activeRuns = new ConcurrentHashMap<>(); private final Map definitions = Collections.synchronizedMap(new WeakHashMap<>()); + private final TreeProvenance provenance = new TreeProvenance(definitions); + private final TreeFellingRunner runner = new TreeFellingRunner(this, provenance); @Override public void onEnable() { @@ -92,7 +78,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { managedEvents.clear(); for (Set runs : activeRuns.values()) { for (FellingRun run : runs) { - finish(run); + runner.finish(run); } } activeRuns.clear(); @@ -105,8 +91,12 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { if (!serviceEnabled.get() || event == null || options == null - || event.isCancelled() - || isManagedBreak(event)) { + || event.isCancelled()) { + return false; + } + // An event that already carries a pending fell may still be upgraded to + // INTEGRATION_OVERRIDE; only internal probes are refused outright. + if (!pending.containsKey(event) && isManagedBreak(event)) { return false; } if (!canUse(event.getPlayer(), options.access())) { @@ -115,7 +105,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { TreeCandidate candidate; try { - candidate = resolveCandidate(event.getBlock(), event.getPlayer()); + candidate = provenance.resolveCandidate(event.getBlock(), event.getPlayer()); } catch (Throwable error) { IrisLogging.reportError("Failed to resolve an Iris tree-feller request.", error); return false; @@ -153,7 +143,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { return false; } try { - return resolveTreeContext(block) != null; + return provenance.resolveTreeContext(block) != null; } catch (Throwable error) { IrisLogging.reportError("Failed to inspect Iris tree provenance.", error); return false; @@ -186,7 +176,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { TreeCandidate current; try { - current = resolveCandidate(event.getBlock(), event.getPlayer()); + current = provenance.resolveCandidate(event.getBlock(), event.getPlayer()); } catch (Throwable error) { IrisLogging.reportError("Failed to finalize an Iris tree-feller request.", error); deferSuccessfulBreakCleanup(event); @@ -224,38 +214,38 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { activeRuns.computeIfAbsent(event.getPlayer().getUniqueId(), ignored -> ConcurrentHashMap.newKeySet()).add(run); notifyActivationAccepted(run.runHooks); run.presentation.activate(event.getBlock()); - discover(run); + runner.discover(run); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void haltWhenSneakingStops(PlayerToggleSneakEvent event) { if (!event.isSneaking()) { - finishRuns(event.getPlayer().getUniqueId()); + runner.finishRuns(event.getPlayer().getUniqueId()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void haltWhenHeldSlotChanges(PlayerItemHeldEvent event) { if (event.getNewSlot() != event.getPreviousSlot()) { - finishRuns(event.getPlayer().getUniqueId()); + runner.finishRuns(event.getPlayer().getUniqueId()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void haltWhenHandsSwap(PlayerSwapHandItemsEvent event) { - finishRuns(event.getPlayer().getUniqueId()); + runner.finishRuns(event.getPlayer().getUniqueId()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void clearPlacedProvenance(BlockPlaceEvent event) { - ProvenanceSnapshot snapshot = captureProvenance(event.getBlockPlaced()); + ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlockPlaced()); if (snapshot == null) { return; } Location location = event.getBlockPlaced().getLocation(); Runnable cleanup = () -> { if (!event.isCancelled()) { - clearProvenanceIfMatching(snapshot); + provenance.clearProvenanceIfMatching(snapshot); } }; if (!J.runAt(location, cleanup, 1) && !J.isFolia()) { @@ -263,6 +253,10 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { } } + boolean isServiceEnabled() { + return serviceEnabled.get(); + } + private boolean canUse(Player player, TreeFellerAccess access) { if (access == TreeFellerAccess.INTEGRATION_OVERRIDE) { return true; @@ -287,616 +281,19 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { } } - private TreeCandidate resolveCandidate(Block block, Player player) { - if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) { - return null; - } - if (!Tag.LOGS.isTagged(block.getType())) { - return null; - } - ItemStack tool = player.getInventory().getItemInMainHand(); - if (!isAxe(tool)) { - return null; - } - TreeContext context = resolveTreeContext(block); - if (context == null) { - return null; - } - return new TreeCandidate( - context, - block.getWorld(), - player, - tool.clone(), - TreeFellerAccess.STANDALONE, - new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ()) - ); - } - - private TreeContext resolveTreeContext(Block block) { - if (block.getType().isAir()) { - return null; - } - PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld()); - if (access == null || access.getEngine() == null) { - return null; - } - Engine engine = access.getEngine(); - World world = block.getWorld(); - int minimumY = world.getMinHeight(); - int maximumY = world.getMaxHeight(); - int relativeY = block.getY() - minimumY; - String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ()); - StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); - if (decoded == null || decoded.structureAware()) { - return null; - } - TreeBlockMaterial expected = engine.getMantle().getMantle().get( - block.getX(), - relativeY, - block.getZ(), - TreeBlockMaterial.class - ); - if (expected != null && !matchesExpectedMaterial(block, expected)) { - return null; - } - if (expected == null - && !decoded.objectKey().startsWith("trees/") - && !definitionIndex(engine).isTreeMarker(marker)) { - return null; - } - return new TreeContext(engine, marker, expected, minimumY, maximumY); - } - - private TreeDefinitionIndex definitionIndex(Engine engine) { - synchronized (definitions) { - return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build); - } - } - - private void discover(FellingRun run) { - J.a(() -> { - if (run.candidate.context().engine().isClosed()) { - finish(run); - return; - } - try { - TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover( - run.candidate.trigger(), - run.candidate.context().marker(), - run.candidate.context().minimumY(), - run.candidate.context().maximumY(), - (x, y, z) -> markerAt( - run.candidate.context().engine(), - run.candidate.context().minimumY(), - x, - y, - z - ) - ); - List positions = positionsForFelling(discovery, run.candidate.trigger()); - preflight(run, positions, discovery.complete()); - } catch (Throwable error) { - IrisLogging.reportError("Failed to discover an Iris tree for felling.", error); - preflight(run, List.of(run.candidate.trigger()), false); - } - }); - } - - private void preflight(FellingRun run, List positions, boolean allowFallback) { - Map> grouped = groupByChunk(positions); - if (grouped.isEmpty()) { - finish(run); - return; - } - Map erosionOrder = new HashMap<>(positions.size()); - for (int index = 0; index < positions.size(); index++) { - erosionOrder.put(positions.get(index), index); - } - - List members = Collections.synchronizedList(new ArrayList<>()); - AtomicBoolean failed = new AtomicBoolean(); - AtomicInteger remaining = new AtomicInteger(grouped.size()); - AtomicBoolean completed = new AtomicBoolean(); - - for (Map.Entry> entry : grouped.entrySet()) { - ChunkPosition chunk = entry.getKey(); - Runnable task = () -> { - try { - if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) { - failed.set(true); - return; - } - for (TreeMarkerTraversal.Position position : entry.getValue()) { - TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE)); - if (member != null) { - members.add(member); - } - } - } catch (Throwable error) { - failed.set(true); - IrisLogging.reportError( - "Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".", - error - ); - } finally { - completePreflightGroup(run, members, failed, remaining, completed, allowFallback); - } - }; - if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) { - failed.set(true); - completePreflightGroup(run, members, failed, remaining, completed, allowFallback); - } - } - } - - static List positionsForFelling( - TreeMarkerTraversal.Discovery discovery, - TreeMarkerTraversal.Position trigger - ) { - return discovery.complete() ? discovery.members() : List.of(trigger); - } - - private void completePreflightGroup( - FellingRun run, - List members, - AtomicBoolean failed, - AtomicInteger remaining, - AtomicBoolean completed, - boolean allowFallback - ) { - if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) { - return; - } - if (failed.get() && allowFallback) { - preflight(run, List.of(run.candidate.trigger()), false); - return; - } - if (failed.get()) { - finish(run); - return; - } - - List ordered = orderMembers(run.candidate.trigger(), members); - if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) { - finish(run); - return; - } - run.work = ordered; - run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size()); - run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse); - processNext(run); - } - - private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) { - World world = run.candidate.world(); - Block block = world.getBlockAt(position.x(), position.y(), position.z()); - TreeContext context = run.candidate.context(); - if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) { - return null; - } - TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position); - if (expected != null && !matchesExpectedMaterial(block, expected)) { - clearProvenance(context.engine(), context.minimumY(), position); - return null; - } - if (block.getType().isAir()) { - clearProvenance(context.engine(), context.minimumY(), position); - return null; - } - return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder); - } - - private List orderMembers( - TreeMarkerTraversal.Position trigger, - Collection discovered - ) { - Comparator erosionOrder = Comparator - .comparingInt(TreeMember::erosionOrder) - .thenComparingInt(member -> member.position().y()) - .thenComparingInt(member -> member.position().x()) - .thenComparingInt(member -> member.position().z()); - List ordered = discovered.stream().sorted(erosionOrder).toList(); - if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) { - return List.of(); - } - return ordered; - } - - private void processNext(FellingRun run) { - if (run.finished.get()) { - return; - } - int index = run.cursor.getAndIncrement(); - if (index >= run.work.size()) { - finish(run); - return; - } - - TreeMember member = run.work.get(index); - ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4); - Runnable task = () -> runTask( - run, - "Failed to prepare an Iris tree-feller block.", - () -> prepareBreak(run, member) - ); - if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) { - if (member.log()) { - finish(run); - } else { - continueRun(run); - } - } - } - - private void prepareBreak(FellingRun run, TreeMember member) { - Block block = liveMemberBlock(run, member); - if (block == null) { - if (member.log()) { - finish(run); - } else { - continueRun(run); - } - return; - } - - if (!member.log()) { - runMutationTask( - run, - member, - new DamageReservation(run.expectedTool.clone(), false, false, false), - new AtomicBoolean() - ); - return; - } - - Runnable task = () -> runTask( - run, - "Failed to reserve Iris tree-feller tool durability.", - () -> reserveDamage(run, member) - ); - if (!J.runEntity(run.candidate.player(), task)) { - finish(run); - } - } - - private void reserveDamage(FellingRun run, TreeMember member) { - Player player = run.candidate.player(); - if (!isRunControlActive(run, player)) { - finish(run); - return; - } - PlayerInventory inventory = player.getInventory(); - ItemStack current = inventory.getItem(run.heldSlot); - if (current == null - || inventory.getHeldItemSlot() != run.heldSlot - || !current.isSimilar(run.expectedTool) - || !isAxe(current)) { - finish(run); - return; - } - - if (!reserveLogCost(run)) { - finish(run); - return; - } - - ItemStack before = current.clone(); - ItemMeta meta = current.getItemMeta(); - if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) { - scheduleMutation(run, member, new DamageReservation(before, false, false, true)); - return; - } - if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) { - refundAndFinish(run, new DamageReservation(before, false, false, true)); - return; - } - - int nextDamage = damageable.getDamage() + 1; - boolean broke = nextDamage >= current.getType().getMaxDurability(); - if (broke) { - inventory.setItem(run.heldSlot, new ItemStack(Material.AIR)); - run.expectedTool = new ItemStack(Material.AIR); - } else { - damageable.setDamage(nextDamage); - current.setItemMeta(meta); - inventory.setItem(run.heldSlot, current); - run.expectedTool = current.clone(); - } - scheduleMutation(run, member, new DamageReservation(before, true, broke, true)); - } - - private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) { - TreeMarkerTraversal.Position position = member.position(); - AtomicBoolean mutationSucceeded = new AtomicBoolean(); - Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded); - boolean scheduled; - try { - scheduled = J.runRegion( - run.candidate.world(), - position.x() >> 4, - position.z() >> 4, - task - ); - } catch (Throwable error) { - IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error); - refundAndFinish(run, reservation); - return; - } - if (!scheduled) { - refundAndFinish(run, reservation); - } - } - - private void runMutationTask( - FellingRun run, - TreeMember member, - DamageReservation reservation, - AtomicBoolean mutationSucceeded - ) { - if (run.finished.get()) { - refundAndFinish(run, reservation); - return; - } - try { - probeAndMutate(run, member, reservation, mutationSucceeded); - } catch (Throwable error) { - IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error); - if (mutationSucceeded.get()) { - finish(run); - } else { - refundAndFinish(run, reservation); - } - } - } - - private void probeAndMutate( - FellingRun run, - TreeMember member, - DamageReservation reservation, - AtomicBoolean mutationSucceeded - ) { - Block block = liveMemberBlock(run, member); - if (block == null) { - refundAndFinish(run, reservation); - return; - } - - BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run); - managedEvents.add(probe); - try { - Bukkit.getPluginManager().callEvent(probe); - } catch (Throwable error) { - probe.setCancelled(true); - IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error); - } finally { - managedEvents.remove(probe); - } - - try { - if (probe.isCancelled()) { - if (reservation.charged() || reservation.logCostReserved()) { - refundAndFinish(run, reservation); - } else if (member.log()) { - finish(run); - } else { - continueRun(run); - } - return; - } - - block = liveMemberBlock(run, member); - if (run.finished.get() || block == null) { - probe.setCancelled(true); - refundAndFinish(run, reservation); - return; - } - - Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D); - BlockData visualData = block.getBlockData().clone(); - List vanillaDrops = probe.isDropItems() - ? block.getDrops(reservation.toolForDrops()).stream() - .map(ItemStack::clone) - .toList() - : List.of(); - block.setType(Material.AIR, false); - if (!block.getType().isAir()) { - probe.setCancelled(true); - refundAndFinish(run, reservation); - return; - } - mutationSucceeded.set(true); - run.presentation.erode( - source, - visualData, - member.erosionOrder(), - run.processed.get(), - run.blocksPerPulse, - run.effectStride, - run.work.size() - ); - - clearProvenance( - run.candidate.context().engine(), - run.candidate.context().minimumY(), - member.position() - ); - routeDrops(run, vanillaDrops, source); - if (!run.presentation.routeExperience(probe.getExpToDrop())) { - dropExperience(source, probe.getExpToDrop()); - } - if (reservation.logCostReserved()) { - completeLogCost(run, reservation); - return; - } - completeSuccessfulMutation(run, reservation); - } catch (RuntimeException | Error error) { - if (!mutationSucceeded.get()) { - probe.setCancelled(true); - } - throw error; - } - } - - private Block liveMemberBlock(FellingRun run, TreeMember member) { - World world = run.candidate.world(); - TreeMarkerTraversal.Position position = member.position(); - if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) { - return null; - } - Block block = world.getBlockAt(position.x(), position.y(), position.z()); - TreeContext context = run.candidate.context(); - if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) { - return null; - } - if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) { - clearProvenance(context.engine(), context.minimumY(), position); - return null; - } - TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position); - if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) { - clearProvenance(context.engine(), context.minimumY(), position); - return null; - } - if (expected != null && !matchesExpectedMaterial(block, expected)) { - clearProvenance(context.engine(), context.minimumY(), position); - return null; - } - return block; - } - - private void refundAndFinish(FellingRun run, DamageReservation reservation) { - if (!reservation.charged() && !reservation.logCostReserved()) { - finish(run); - return; - } - if (!J.runEntity(run.candidate.player(), () -> { - if (reservation.charged()) { - PlayerInventory inventory = run.candidate.player().getInventory(); - ItemStack current = inventory.getItem(run.heldSlot); - boolean expectedAir = run.expectedTool.getType() == Material.AIR; - boolean currentMatches = expectedAir - ? current == null || current.getType() == Material.AIR - : current != null && current.isSimilar(run.expectedTool); - if (currentMatches) { - inventory.setItem(run.heldSlot, reservation.toolForDrops().clone()); - run.expectedTool = reservation.toolForDrops().clone(); - } - } - if (reservation.logCostReserved()) { - refundLogCost(run); - } - finish(run); - })) { - finish(run); - } - } - - private boolean isRunControlActive(FellingRun run, Player player) { - return player.isOnline() - && player.getGameMode() == GameMode.SURVIVAL - && player.isSneaking() - && player.getWorld().equals(run.candidate.world()); - } - - private boolean reserveLogCost(FellingRun run) { - try { - return run.runHooks.reserveLogCost(); - } catch (Throwable error) { - IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error); - return false; - } - } - - private void completeLogCost(FellingRun run, DamageReservation reservation) { - if (!J.runEntity(run.candidate.player(), () -> { - try { - run.runHooks.commitLogCost(); - } catch (Throwable error) { - IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error); - finish(run); - return; - } - completeSuccessfulMutation(run, reservation); - })) { - finish(run); - } - } - - private void refundLogCost(FellingRun run) { - try { - run.runHooks.refundLogCost(); - } catch (Throwable error) { - IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error); - } - } - - private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) { - if (reservation.broke()) { - finish(run); - return; - } - continueRun(run); - } - - private void continueRun(FellingRun run) { - int processed = run.processed.incrementAndGet(); - if (processed % run.blocksPerPulse == 0) { - J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1); - } else { - processNext(run); - } - } - - private void runTask(FellingRun run, String context, Runnable task) { - if (run.finished.get() || !serviceEnabled.get()) { - finish(run); - return; - } - try { - task.run(); - } catch (Throwable error) { - IrisLogging.reportError(context, error); - finish(run); - } - } - - private void finish(FellingRun run) { - if (run.finished.compareAndSet(false, true)) { - activeClaims.remove(run.claim); - Set runs = activeRuns.get(run.candidate.player().getUniqueId()); - if (runs != null) { - runs.remove(run); - if (runs.isEmpty()) { - activeRuns.remove(run.candidate.player().getUniqueId(), runs); - } - } - run.presentation.finish(); - } - } - - private void finishRuns(UUID playerId) { - Set runs = activeRuns.get(playerId); - if (runs == null) { - return; - } - for (FellingRun run : List.copyOf(runs)) { - finish(run); - } - } - private void releaseManagedLater(BlockBreakEvent event) { J.s(() -> managedEvents.remove(event), 1); } private void deferSuccessfulBreakCleanup(BlockBreakEvent event) { - ProvenanceSnapshot snapshot = captureProvenance(event.getBlock()); + ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlock()); if (snapshot == null) { return; } Location location = event.getBlock().getLocation(); Runnable cleanup = () -> { if (!event.isCancelled()) { - clearProvenanceIfMatching(snapshot); + provenance.clearProvenanceIfMatching(snapshot); } }; if (!J.runAt(location, cleanup, 1) && !J.isFolia()) { @@ -904,77 +301,11 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { } } - private Map> groupByChunk( - List positions - ) { - Map> grouped = new LinkedHashMap<>(); - for (TreeMarkerTraversal.Position position : positions) { - ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4); - grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position); - } - return grouped; - } - - private String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { - return markerAt(engine, minimumY, position.x(), position.y(), position.z()); - } - - private String markerAt(Engine engine, int minimumY, int x, int y, int z) { - return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class); - } - - private TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { - return engine.getMantle().getMantle().get( - position.x(), - position.y() - minimumY, - position.z(), - TreeBlockMaterial.class - ); - } - - private ProvenanceSnapshot captureProvenance(Block block) { - PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld()); - if (access == null || access.getEngine() == null) { - return null; - } - Engine engine = access.getEngine(); - TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position( - block.getX(), - block.getY(), - block.getZ() - ); - int minimumY = block.getWorld().getMinHeight(); - String marker = markerAt(engine, minimumY, position); - TreeBlockMaterial material = materialAt(engine, minimumY, position); - if (marker == null && material == null) { - return null; - } - return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material); - } - - private void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) { - String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position()); - TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position()); - if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) { - clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position()); - } - } - - private void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { - int relativeY = position.y() - minimumY; - engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class); - engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class); - } - - private boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) { - return expected.matches(block.getBlockData().getAsString()); - } - - private boolean isAxe(ItemStack item) { + static boolean isAxe(ItemStack item) { return item != null && item.getType() != Material.AIR && item.getType().name().endsWith("_AXE"); } - private void routeDrops(FellingRun run, List drops, Location source) { + void routeDrops(FellingRun run, List drops, Location source) { World world = source.getWorld(); if (world == null) { return; @@ -986,7 +317,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { } } - private void dropExperience(Location location, int experience) { + void dropExperience(Location location, int experience) { if (experience <= 0 || location.getWorld() == null) { return; } @@ -996,115 +327,4 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService { ); orb.setExperience(experience); } - - private record PendingFell( - TreeCandidate candidate, - int preservationChance, - TreeFellerRunHooks runHooks - ) { - private PendingFell withAccess(TreeFellerAccess access) { - return new PendingFell(candidate.withAccess(access), preservationChance, runHooks); - } - } - - private record TreeCandidate( - TreeContext context, - World world, - Player player, - ItemStack tool, - TreeFellerAccess access, - TreeMarkerTraversal.Position trigger - ) { - private TreeCandidate withAccess(TreeFellerAccess access) { - return new TreeCandidate(context, world, player, tool, access, trigger); - } - } - - private record TreeContext( - Engine engine, - String marker, - TreeBlockMaterial expectedMaterial, - int minimumY, - int maximumY - ) { - } - - private record TreeClaim(UUID worldId, String marker) { - } - - private record ProvenanceSnapshot( - Engine engine, - World world, - int minimumY, - TreeMarkerTraversal.Position position, - String marker, - TreeBlockMaterial material - ) { - } - - private record ChunkPosition(int x, int z) { - } - - private record TreeMember( - TreeMarkerTraversal.Position position, - boolean log, - TreeBlockMaterial expectedMaterial, - int erosionOrder - ) { - } - - private record DamageReservation( - ItemStack toolForDrops, - boolean charged, - boolean broke, - boolean logCostReserved - ) { - } - - private final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter { - private final FellingRun run; - - private RoutedBlockBreakEvent(Block block, Player player, FellingRun run) { - super(block, player); - this.run = run; - } - - @Override - public boolean routeDrop(Object drop) { - return serviceEnabled.get() && run.presentation.routeDrop(drop); - } - } - - private static final class FellingRun { - private final TreeClaim claim; - private final TreeCandidate candidate; - private final int preservationChance; - private final TreeFellerRunHooks runHooks; - private final int heldSlot; - private final TreeFellerPresentation presentation; - private final AtomicBoolean finished = new AtomicBoolean(); - private final AtomicInteger cursor = new AtomicInteger(); - private final AtomicInteger processed = new AtomicInteger(); - private volatile int blocksPerPulse = 1; - private volatile int effectStride = 1; - private volatile ItemStack expectedTool; - private volatile List work = List.of(); - - private FellingRun( - TreeClaim claim, - TreeCandidate candidate, - int preservationChance, - TreeFellerRunHooks runHooks, - int heldSlot, - Location fallbackLocation - ) { - this.claim = claim; - this.candidate = candidate; - this.preservationChance = preservationChance; - this.runHooks = runHooks; - this.heldSlot = heldSlot; - this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation); - this.expectedTool = candidate.tool().clone(); - } - } } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java new file mode 100644 index 000000000..ee7bf83b3 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeFellingRunner.java @@ -0,0 +1,590 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.core.service.TreeFellerModel.ChunkPosition; +import art.arcane.iris.core.service.TreeFellerModel.DamageReservation; +import art.arcane.iris.core.service.TreeFellerModel.TreeContext; +import art.arcane.iris.core.service.TreeFellerModel.TreeMember; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +final class TreeFellingRunner { + private final TreeFellerSVC service; + private final TreeProvenance provenance; + + TreeFellingRunner(TreeFellerSVC service, TreeProvenance provenance) { + this.service = service; + this.provenance = provenance; + } + + void discover(FellingRun run) { + J.a(() -> { + if (run.candidate.context().engine().isClosed()) { + finish(run); + return; + } + try { + TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover( + run.candidate.trigger(), + run.candidate.context().marker(), + run.candidate.context().minimumY(), + run.candidate.context().maximumY(), + (x, y, z) -> provenance.markerAt( + run.candidate.context().engine(), + run.candidate.context().minimumY(), + x, + y, + z + ) + ); + List positions = positionsForFelling(discovery, run.candidate.trigger()); + preflight(run, positions, discovery.complete()); + } catch (Throwable error) { + IrisLogging.reportError("Failed to discover an Iris tree for felling.", error); + preflight(run, List.of(run.candidate.trigger()), false); + } + }); + } + + private void preflight(FellingRun run, List positions, boolean allowFallback) { + Map> grouped = groupByChunk(positions); + if (grouped.isEmpty()) { + finish(run); + return; + } + Map erosionOrder = new HashMap<>(positions.size()); + for (int index = 0; index < positions.size(); index++) { + erosionOrder.put(positions.get(index), index); + } + + List members = Collections.synchronizedList(new ArrayList<>()); + AtomicBoolean failed = new AtomicBoolean(); + AtomicInteger remaining = new AtomicInteger(grouped.size()); + AtomicBoolean completed = new AtomicBoolean(); + + for (Map.Entry> entry : grouped.entrySet()) { + ChunkPosition chunk = entry.getKey(); + Runnable task = () -> { + try { + if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) { + failed.set(true); + return; + } + for (TreeMarkerTraversal.Position position : entry.getValue()) { + TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE)); + if (member != null) { + members.add(member); + } + } + } catch (Throwable error) { + failed.set(true); + IrisLogging.reportError( + "Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".", + error + ); + } finally { + completePreflightGroup(run, members, failed, remaining, completed, allowFallback); + } + }; + if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) { + failed.set(true); + completePreflightGroup(run, members, failed, remaining, completed, allowFallback); + } + } + } + + static List positionsForFelling( + TreeMarkerTraversal.Discovery discovery, + TreeMarkerTraversal.Position trigger + ) { + return discovery.complete() ? discovery.members() : List.of(trigger); + } + + private void completePreflightGroup( + FellingRun run, + List members, + AtomicBoolean failed, + AtomicInteger remaining, + AtomicBoolean completed, + boolean allowFallback + ) { + if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) { + return; + } + if (failed.get() && allowFallback) { + preflight(run, List.of(run.candidate.trigger()), false); + return; + } + if (failed.get()) { + finish(run); + return; + } + + List ordered = orderMembers(run.candidate.trigger(), members); + if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) { + finish(run); + return; + } + run.work = ordered; + run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size()); + run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse); + processNext(run); + } + + private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) { + World world = run.candidate.world(); + Block block = world.getBlockAt(position.x(), position.y(), position.z()); + TreeContext context = run.candidate.context(); + if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) { + return null; + } + TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position); + if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + if (block.getType().isAir()) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder); + } + + private List orderMembers( + TreeMarkerTraversal.Position trigger, + Collection discovered + ) { + Comparator erosionOrder = Comparator + .comparingInt(TreeMember::erosionOrder) + .thenComparingInt(member -> member.position().y()) + .thenComparingInt(member -> member.position().x()) + .thenComparingInt(member -> member.position().z()); + List ordered = discovered.stream().sorted(erosionOrder).toList(); + if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) { + return List.of(); + } + return ordered; + } + + private void processNext(FellingRun run) { + if (run.finished.get()) { + return; + } + int index = run.cursor.getAndIncrement(); + if (index >= run.work.size()) { + finish(run); + return; + } + + TreeMember member = run.work.get(index); + ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4); + Runnable task = () -> runTask( + run, + "Failed to prepare an Iris tree-feller block.", + () -> prepareBreak(run, member) + ); + if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) { + if (member.log()) { + finish(run); + } else { + continueRun(run); + } + } + } + + private void prepareBreak(FellingRun run, TreeMember member) { + Block block = liveMemberBlock(run, member); + if (block == null) { + if (member.log()) { + finish(run); + } else { + continueRun(run); + } + return; + } + + if (!member.log()) { + runMutationTask( + run, + member, + new DamageReservation(run.expectedTool.clone(), false, false, false), + new AtomicBoolean() + ); + return; + } + + Runnable task = () -> runTask( + run, + "Failed to reserve Iris tree-feller tool durability.", + () -> reserveDamage(run, member) + ); + if (!J.runEntity(run.candidate.player(), task)) { + finish(run); + } + } + + private void reserveDamage(FellingRun run, TreeMember member) { + Player player = run.candidate.player(); + if (!isRunControlActive(run, player)) { + finish(run); + return; + } + PlayerInventory inventory = player.getInventory(); + ItemStack current = inventory.getItem(run.heldSlot); + if (current == null + || inventory.getHeldItemSlot() != run.heldSlot + || !current.isSimilar(run.expectedTool) + || !TreeFellerSVC.isAxe(current)) { + finish(run); + return; + } + + if (!reserveLogCost(run)) { + finish(run); + return; + } + + ItemStack before = current.clone(); + ItemMeta meta = current.getItemMeta(); + if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) { + scheduleMutation(run, member, new DamageReservation(before, false, false, true)); + return; + } + if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) { + refundAndFinish(run, new DamageReservation(before, false, false, true)); + return; + } + + int nextDamage = damageable.getDamage() + 1; + boolean broke = nextDamage >= current.getType().getMaxDurability(); + if (broke) { + inventory.setItem(run.heldSlot, new ItemStack(Material.AIR)); + run.expectedTool = new ItemStack(Material.AIR); + } else { + damageable.setDamage(nextDamage); + current.setItemMeta(meta); + inventory.setItem(run.heldSlot, current); + run.expectedTool = current.clone(); + } + scheduleMutation(run, member, new DamageReservation(before, true, broke, true)); + } + + private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) { + TreeMarkerTraversal.Position position = member.position(); + AtomicBoolean mutationSucceeded = new AtomicBoolean(); + Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded); + boolean scheduled; + try { + scheduled = J.runRegion( + run.candidate.world(), + position.x() >> 4, + position.z() >> 4, + task + ); + } catch (Throwable error) { + IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error); + refundAndFinish(run, reservation); + return; + } + if (!scheduled) { + refundAndFinish(run, reservation); + } + } + + private void runMutationTask( + FellingRun run, + TreeMember member, + DamageReservation reservation, + AtomicBoolean mutationSucceeded + ) { + if (run.finished.get()) { + refundAndFinish(run, reservation); + return; + } + try { + probeAndMutate(run, member, reservation, mutationSucceeded); + } catch (Throwable error) { + IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error); + if (mutationSucceeded.get()) { + finish(run); + } else { + refundAndFinish(run, reservation); + } + } + } + + private void probeAndMutate( + FellingRun run, + TreeMember member, + DamageReservation reservation, + AtomicBoolean mutationSucceeded + ) { + Block block = liveMemberBlock(run, member); + if (block == null) { + refundAndFinish(run, reservation); + return; + } + + BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service); + service.managedEvents.add(probe); + try { + Bukkit.getPluginManager().callEvent(probe); + } catch (Throwable error) { + probe.setCancelled(true); + IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error); + } finally { + service.managedEvents.remove(probe); + } + + try { + if (probe.isCancelled()) { + if (reservation.charged() || reservation.logCostReserved()) { + refundAndFinish(run, reservation); + } else if (member.log()) { + finish(run); + } else { + continueRun(run); + } + return; + } + + block = liveMemberBlock(run, member); + if (run.finished.get() || block == null) { + probe.setCancelled(true); + refundAndFinish(run, reservation); + return; + } + + Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D); + BlockData visualData = block.getBlockData().clone(); + List vanillaDrops = probe.isDropItems() + ? block.getDrops(reservation.toolForDrops()).stream() + .map(ItemStack::clone) + .toList() + : List.of(); + block.setType(Material.AIR, false); + if (!block.getType().isAir()) { + probe.setCancelled(true); + refundAndFinish(run, reservation); + return; + } + mutationSucceeded.set(true); + run.presentation.erode( + source, + visualData, + member.erosionOrder(), + run.processed.get(), + run.blocksPerPulse, + run.effectStride, + run.work.size() + ); + + provenance.clearProvenance( + run.candidate.context().engine(), + run.candidate.context().minimumY(), + member.position() + ); + service.routeDrops(run, vanillaDrops, source); + if (!run.presentation.routeExperience(probe.getExpToDrop())) { + service.dropExperience(source, probe.getExpToDrop()); + } + if (reservation.logCostReserved()) { + completeLogCost(run, reservation); + return; + } + completeSuccessfulMutation(run, reservation); + } catch (RuntimeException | Error error) { + if (!mutationSucceeded.get()) { + probe.setCancelled(true); + } + throw error; + } + } + + private Block liveMemberBlock(FellingRun run, TreeMember member) { + World world = run.candidate.world(); + TreeMarkerTraversal.Position position = member.position(); + if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) { + return null; + } + Block block = world.getBlockAt(position.x(), position.y(), position.z()); + TreeContext context = run.candidate.context(); + if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) { + return null; + } + if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position); + if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) { + provenance.clearProvenance(context.engine(), context.minimumY(), position); + return null; + } + return block; + } + + private void refundAndFinish(FellingRun run, DamageReservation reservation) { + if (!reservation.charged() && !reservation.logCostReserved()) { + finish(run); + return; + } + if (!J.runEntity(run.candidate.player(), () -> { + if (reservation.charged()) { + PlayerInventory inventory = run.candidate.player().getInventory(); + ItemStack current = inventory.getItem(run.heldSlot); + boolean expectedAir = run.expectedTool.getType() == Material.AIR; + boolean currentMatches = expectedAir + ? current == null || current.getType() == Material.AIR + : current != null && current.isSimilar(run.expectedTool); + if (currentMatches) { + inventory.setItem(run.heldSlot, reservation.toolForDrops().clone()); + run.expectedTool = reservation.toolForDrops().clone(); + } + } + if (reservation.logCostReserved()) { + refundLogCost(run); + } + finish(run); + })) { + finish(run); + } + } + + private boolean isRunControlActive(FellingRun run, Player player) { + return player.isOnline() + && player.getGameMode() == GameMode.SURVIVAL + && player.isSneaking() + && player.getWorld().equals(run.candidate.world()); + } + + private boolean reserveLogCost(FellingRun run) { + try { + return run.runHooks.reserveLogCost(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error); + return false; + } + } + + private void completeLogCost(FellingRun run, DamageReservation reservation) { + if (!J.runEntity(run.candidate.player(), () -> { + try { + run.runHooks.commitLogCost(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error); + finish(run); + return; + } + completeSuccessfulMutation(run, reservation); + })) { + finish(run); + } + } + + private void refundLogCost(FellingRun run) { + try { + run.runHooks.refundLogCost(); + } catch (Throwable error) { + IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error); + } + } + + private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) { + if (reservation.broke()) { + finish(run); + return; + } + continueRun(run); + } + + private void continueRun(FellingRun run) { + int processed = run.processed.incrementAndGet(); + if (processed % run.blocksPerPulse == 0) { + J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1); + } else { + processNext(run); + } + } + + private void runTask(FellingRun run, String context, Runnable task) { + if (run.finished.get() || !service.isServiceEnabled()) { + finish(run); + return; + } + try { + task.run(); + } catch (Throwable error) { + IrisLogging.reportError(context, error); + finish(run); + } + } + + void finish(FellingRun run) { + if (run.finished.compareAndSet(false, true)) { + service.activeClaims.remove(run.claim); + Set runs = service.activeRuns.get(run.candidate.player().getUniqueId()); + if (runs != null) { + runs.remove(run); + if (runs.isEmpty()) { + service.activeRuns.remove(run.candidate.player().getUniqueId(), runs); + } + } + run.presentation.finish(); + } + } + + void finishRuns(UUID playerId) { + Set runs = service.activeRuns.get(playerId); + if (runs == null) { + return; + } + for (FellingRun run : List.copyOf(runs)) { + finish(run); + } + } + + private Map> groupByChunk( + List positions + ) { + Map> grouped = new LinkedHashMap<>(); + for (TreeMarkerTraversal.Position position : positions) { + ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4); + grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position); + } + return grouped; + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java new file mode 100644 index 000000000..273073372 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/TreeProvenance.java @@ -0,0 +1,151 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.api.tree.TreeFellerAccess; +import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot; +import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate; +import art.arcane.iris.core.service.TreeFellerModel.TreeContext; +import art.arcane.iris.core.service.tree.TreeDefinitionIndex; +import art.arcane.iris.core.service.tree.TreeMarkerTraversal; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.StructurePlacementMarker; +import art.arcane.iris.engine.framework.TreeBlockMaterial; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import org.bukkit.GameMode; +import org.bukkit.Tag; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; +import java.util.Objects; + +final class TreeProvenance { + private final Map definitions; + + TreeProvenance(Map definitions) { + this.definitions = definitions; + } + + TreeCandidate resolveCandidate(Block block, Player player) { + if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) { + return null; + } + if (!Tag.LOGS.isTagged(block.getType())) { + return null; + } + ItemStack tool = player.getInventory().getItemInMainHand(); + if (!TreeFellerSVC.isAxe(tool)) { + return null; + } + TreeContext context = resolveTreeContext(block); + if (context == null) { + return null; + } + return new TreeCandidate( + context, + block.getWorld(), + player, + tool.clone(), + TreeFellerAccess.STANDALONE, + new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ()) + ); + } + + TreeContext resolveTreeContext(Block block) { + if (block.getType().isAir()) { + return null; + } + PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld()); + if (access == null || access.getEngine() == null) { + return null; + } + Engine engine = access.getEngine(); + World world = block.getWorld(); + int minimumY = world.getMinHeight(); + int maximumY = world.getMaxHeight(); + int relativeY = block.getY() - minimumY; + String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ()); + StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker); + if (decoded == null || decoded.structureAware()) { + return null; + } + TreeBlockMaterial expected = engine.getMantle().getMantle().get( + block.getX(), + relativeY, + block.getZ(), + TreeBlockMaterial.class + ); + if (expected != null && !matchesExpectedMaterial(block, expected)) { + return null; + } + if (expected == null + && !decoded.objectKey().startsWith("trees/") + && !definitionIndex(engine).isTreeMarker(marker)) { + return null; + } + return new TreeContext(engine, marker, expected, minimumY, maximumY); + } + + TreeDefinitionIndex definitionIndex(Engine engine) { + synchronized (definitions) { + return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build); + } + } + + String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { + return markerAt(engine, minimumY, position.x(), position.y(), position.z()); + } + + String markerAt(Engine engine, int minimumY, int x, int y, int z) { + return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class); + } + + TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { + return engine.getMantle().getMantle().get( + position.x(), + position.y() - minimumY, + position.z(), + TreeBlockMaterial.class + ); + } + + ProvenanceSnapshot captureProvenance(Block block) { + PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld()); + if (access == null || access.getEngine() == null) { + return null; + } + Engine engine = access.getEngine(); + TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position( + block.getX(), + block.getY(), + block.getZ() + ); + int minimumY = block.getWorld().getMinHeight(); + String marker = markerAt(engine, minimumY, position); + TreeBlockMaterial material = materialAt(engine, minimumY, position); + if (marker == null && material == null) { + return null; + } + return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material); + } + + void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) { + String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position()); + TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position()); + if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) { + clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position()); + } + } + + void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) { + int relativeY = position.y() - minimumY; + engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class); + engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class); + } + + boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) { + return expected.matches(block.getBlockData().getAsString()); + } +} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java index f9a6199df..9e6b57eaf 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/WandSVC.java @@ -361,11 +361,7 @@ public class WandSVC implements IrisService { wand = createWand(); dust = createDust(); - J.ar(() -> { - for (Player i : Bukkit.getOnlinePlayers()) { - tick(i); - } - }, 0); + J.ar(this::tickAll, 0); } @Override @@ -373,6 +369,22 @@ public class WandSVC implements IrisService { } + /** + * Async driver tick. The online player list is only read from the thread that owns it, + * and every wand draw is dispatched to the thread owning that player. + */ + private void tickAll() { + try { + J.runGlobal(() -> { + for (Player p : Bukkit.getOnlinePlayers()) { + J.runEntity(p, () -> tick(p)); + } + }); + } catch (Throwable e) { + Iris.reportError(e); + } + } + public void tick(Player p) { try { try { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java index 11beae8d8..8e110b85b 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisColumnWalk.java @@ -22,9 +22,9 @@ public final class IrisColumnWalk { int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4); int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15); - for (int blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) { - for (int blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) { - if (!visitor.visit(blockX, blockZ)) { + for (long blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) { + for (long blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) { + if (!visitor.visit((int) blockX, (int) blockZ)) { return visited; } visited++; @@ -36,10 +36,10 @@ public final class IrisColumnWalk { return visited; } - private static int align(int origin, int lowerBound, int stride) { + private static long align(int origin, int lowerBound, int stride) { long offset = (long) lowerBound - (long) origin; long steps = (offset + stride - 1L) / stride; - return (int) (origin + steps * stride); + return origin + steps * stride; } @FunctionalInterface diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java index 48d1aba23..3808a2f31 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/wand/WandSelection.java @@ -41,6 +41,10 @@ public class WandSelection { public void draw() { Location playerLoc = p.getLocation(); + if (c.getWorld() == null || !c.getWorld().equals(playerLoc.getWorld())) { + return; + } + double maxDistanceSquared = 256 * 256; int particleCount = 0; diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java index e3096e9eb..6aec41a00 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/TreeFellerEventOrderTest.java @@ -37,7 +37,7 @@ public class TreeFellerEventOrderTest { false ); - assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger)); + assertEquals(List.of(trigger), TreeFellingRunner.positionsForFelling(incomplete, trigger)); } @Test diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java index 8aee38ea3..e50cf361b 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileAssembler.java @@ -1,21 +1,32 @@ package art.arcane.iris.client; import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.spi.protocol.IrisProtocol; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; final class IrisTileAssembler { + private static final int MAX_CHUNK_COUNT = + (IrisTileCodec.MAX_DECODED_BYTES + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES - 1) + / IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES + 1; + private static final int MAX_PENDING_TILES = 64; + private final Map partials; IrisTileAssembler() { - this.partials = new HashMap<>(); + this.partials = new LinkedHashMap<>() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_PENDING_TILES; + } + }; } IrisTileImage add(IrisMessage.VisionTile tile) { int chunkCount = tile.chunkCount(); int chunkIndex = tile.chunkIndex(); - if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) { + if (chunkCount <= 0 || chunkCount > MAX_CHUNK_COUNT || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) { return null; } IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel()); diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java index 1461bfdc4..b325db4a5 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisTileCodec.java @@ -12,6 +12,7 @@ public final class IrisTileCodec { public static final int MODE_PALETTE = 1; private static final int MAX_DIMENSION = 512; private static final int OPAQUE = 0xFF000000; + static final int MAX_DECODED_BYTES = 9 + 4 + 3 * MAX_DIMENSION * MAX_DIMENSION; private IrisTileCodec() { } @@ -87,6 +88,9 @@ public final class IrisTileCodec { break; } } + if (out.size() + produced > MAX_DECODED_BYTES) { + return null; + } out.write(buffer, 0, produced); } } catch (DataFormatException malformed) { diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index ab92d6f8b..54d865304 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -103,8 +103,11 @@ configurations.runtimeClasspath.extendsFrom(configurations.devBundle) configurations.testCompileClasspath.extendsFrom(configurations.devBundle) configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle) +// Jar-in-jar payload: exactly the Fabric API modules declared below, nothing else. Kept +// non-transitive so the bundled set matches the `jars` list in fabric.mod.json one-for-one — a +// transitive pull (fabric-transitive-access-wideners-v1) used to land in META-INF/jars undeclared. configurations.create('jij') { - transitive = true + transitive = false } dependencies { diff --git a/adapters/fabric/settings.gradle b/adapters/fabric/settings.gradle index c7106b7e7..c46bcfa7c 100644 --- a/adapters/fabric/settings.gradle +++ b/adapters/fabric/settings.gradle @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import java.io.File - pluginManagement { repositories { maven { @@ -43,49 +41,8 @@ dependencyResolutionManagement { } } -boolean hasVolmLibSettings(File directory) { - new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists() -} - -File resolveLocalVolmLibDirectory() { - String configuredPath = providers.gradleProperty('localVolmLibDirectory') - .orElse(providers.environmentVariable('VOLMLIB_DIR')) - .orNull - if (configuredPath != null && !configuredPath.isBlank()) { - File configuredDirectory = file(configuredPath) - if (hasVolmLibSettings(configuredDirectory)) { - return configuredDirectory - } - } - - File currentDirectory = settingsDir - while (currentDirectory != null) { - File candidate = new File(currentDirectory, 'VolmLib') - if (hasVolmLibSettings(candidate)) { - return candidate - } - - currentDirectory = currentDirectory.parentFile - } - - null -} - -boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib') - .orElse('true') - .map { String value -> value.equalsIgnoreCase('true') } - .get() -File localVolmLibDirectory = resolveLocalVolmLibDirectory() - -if (useLocalVolmLib && localVolmLibDirectory != null) { - includeBuild(localVolmLibDirectory) { - dependencySubstitution { - substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared')) - } - } -} +// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle. +apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile includeBuild('../..') { dependencySubstitution { diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java index ee5e19a60..1b2669521 100644 --- a/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/mixin/PackRepositoryMixin.java @@ -26,6 +26,9 @@ 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; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; @Mixin(PackRepository.class) public class PackRepositoryMixin { @@ -37,5 +40,10 @@ public class PackRepositoryMixin { return; } } + // Client resource-pack repositories legitimately have no ServerPacksSource; a missing server-data + // repository is reported once at boot by ModdedForcedDatapack.verifyInjected(). + LoggerFactory.getLogger("Iris").debug( + "Iris forced datapack source not attached: no ServerPacksSource among {} source(s) {}", + sources.length, Arrays.toString(sources)); } } diff --git a/adapters/fabric/src/main/resources/fabric.mod.json b/adapters/fabric/src/main/resources/fabric.mod.json index d47ac6abd..5e5387e48 100644 --- a/adapters/fabric/src/main/resources/fabric.mod.json +++ b/adapters/fabric/src/main/resources/fabric.mod.json @@ -28,12 +28,12 @@ { "file": "META-INF/jars/fabric-registry-sync-v0.jar" }, { "file": "META-INF/jars/fabric-resource-loader-v1.jar" }, { "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" }, - { "file": "META-INF/jars/fabric-networking-api-v1.jar" }, { "file": "META-INF/jars/fabric-command-api-v2.jar" }, { "file": "META-INF/jars/fabric-events-interaction-v0.jar" }, + { "file": "META-INF/jars/fabric-networking-api-v1.jar" }, { "file": "META-INF/jars/fabric-rendering-v1.jar" }, - { "file": "META-INF/jars/fabric-transitive-access-wideners-v1.jar" }, - { "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" } + { "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }, + { "file": "META-INF/jars/fabric-permission-api-v1.jar" } ], "depends": { "fabricloader": ">=0.19.3", diff --git a/adapters/forge/settings.gradle b/adapters/forge/settings.gradle index f17c1537e..d24d880c0 100644 --- a/adapters/forge/settings.gradle +++ b/adapters/forge/settings.gradle @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import java.io.File - pluginManagement { repositories { gradlePluginPortal() @@ -43,49 +41,8 @@ dependencyResolutionManagement { } } -boolean hasVolmLibSettings(File directory) { - new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists() -} - -File resolveLocalVolmLibDirectory() { - String configuredPath = providers.gradleProperty('localVolmLibDirectory') - .orElse(providers.environmentVariable('VOLMLIB_DIR')) - .orNull - if (configuredPath != null && !configuredPath.isBlank()) { - File configuredDirectory = file(configuredPath) - if (hasVolmLibSettings(configuredDirectory)) { - return configuredDirectory - } - } - - File currentDirectory = settingsDir - while (currentDirectory != null) { - File candidate = new File(currentDirectory, 'VolmLib') - if (hasVolmLibSettings(candidate)) { - return candidate - } - - currentDirectory = currentDirectory.parentFile - } - - null -} - -boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib') - .orElse('true') - .map { String value -> value.equalsIgnoreCase('true') } - .get() -File localVolmLibDirectory = resolveLocalVolmLibDirectory() - -if (useLocalVolmLib && localVolmLibDirectory != null) { - includeBuild(localVolmLibDirectory) { - dependencySubstitution { - substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared')) - } - } -} +// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle. +apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile includeBuild('../..') { dependencySubstitution { diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java index f049e3ecd..f1144b92d 100644 --- a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFactory.java @@ -63,10 +63,10 @@ public final class NativeStructureFactory { } StructureStart positioned; if (plan.placement().isUnderground()) { - positioned = NativeStructurePostProcessor.relocateToMinY( + positioned = NativeStructureVerticalPlacer.relocateToMinY( generated, source, plan.baseY(), context.heightAccessor()); } else { - NativeStructurePostProcessor.applyVerticalPlacement( + NativeStructureVerticalPlacer.applyVerticalPlacement( generated, plan.source().getStructure(), 0, diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java new file mode 100644 index 000000000..c5b2f71a7 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureFoundationBuilder.java @@ -0,0 +1,215 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisStructureStiltSettings; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SupportType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureFoundationBuilder { + private static final int FOUNDATION_VERTICAL_TOLERANCE = 1; + + private NativeStructureFoundationBuilder() { + } + + private static List foundationEnvelope(BoundingBox area, StructureStart start) { + BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start); + int minX = Math.max(area.minX(), structure.minX()); + int minZ = Math.max(area.minZ(), structure.minZ()); + int maxX = Math.min(area.maxX(), structure.maxX()); + int maxZ = Math.min(area.maxZ(), structure.maxZ()); + if (minX > maxX || minZ > maxZ) { + return List.of(); + } + List pieces = start.getPieces(); + List columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1)); + BitSet envelope = new BitSet(area.getYSpan()); + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + envelope.clear(); + markFoundationEnvelope(envelope, pieces, area, x, z); + int cellCount = envelope.cardinality(); + if (cellCount == 0) { + continue; + } + int[] ys = new int[cellCount]; + int cell = 0; + for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) { + ys[cell++] = area.minY() + bit; + } + columns.add(new FoundationColumn(x, z, ys)); + } + } + return List.copyOf(columns); + } + + private static void markFoundationEnvelope(BitSet envelope, List pieces, BoundingBox area, + int x, int z) { + for (StructurePiece piece : pieces) { + if (NativeStructureReferenceEnvelope.isMarker(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) { + continue; + } + int groundY = bounds.minY(); + if (piece instanceof PoolElementStructurePiece poolPiece) { + groundY += poolPiece.getGroundLevelDelta(); + if (groundY < bounds.minY()) { + continue; + } + } + int minY = Math.max(area.minY(), bounds.minY()); + int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE)); + if (minY <= maxY) { + envelope.set(minY - area.minY(), maxY - area.minY() + 1); + } + } + } + + static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureStiltSettings settings, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver, + IntBinaryOperator surfaceHeight, + boolean surfaceStructure) { + Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver"); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + int structureHash = structureId == null ? 0 : structureId.hashCode(); + RNG rng = new RNG(world.getSeed() ^ structureHash); + for (FoundationColumn column : foundationEnvelope(area, start)) { + if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) { + continue; + } + int foundationY = findFoundationY(world, column, position); + if (foundationY == Integer.MIN_VALUE) { + continue; + } + int terrainY = surfaceStructure + ? Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))) + : area.minY() - 1; + int anchorY = findStiltAnchorY( + world, column.x(), column.z(), foundationY, + Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position); + if (anchorY == Integer.MIN_VALUE) { + continue; + } + for (int y = foundationY - 1; y > anchorY; y--) { + position.set(column.x(), y, column.z()); + BlockState stilt = settings.getPalette() == null + ? Blocks.COBBLESTONE.defaultBlockState() + : Objects.requireNonNull( + paletteBlockResolver.resolve( + settings.getPalette(), rng, column.x(), y, column.z()), + "Stilt palette returned no block for " + structureId + " at " + + column.x() + "," + y + "," + column.z()); + world.setBlock(position, stilt, 2); + } + } + } + + static boolean isStiltColumn(int x, int z, int spacing) { + int resolvedSpacing = Math.max(1, spacing); + return resolvedSpacing == 1 + || Math.floorMod(x, resolvedSpacing) == 0 + && Math.floorMod(z, resolvedSpacing) == 0; + } + + static int findStiltAnchorY( + WorldGenLevel world, int x, int z, int foundationY, int maxDepth, + int terrainY, int areaMinY, BlockPos.MutableBlockPos position) { + int minimumAnchorY = Math.max( + areaMinY, Math.max(terrainY, foundationY - maxDepth - 1)); + for (int y = foundationY - 1; y >= minimumAnchorY; y--) { + BlockState state = world.getBlockState(position.set(x, y, z)); + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + if (!vegetation && state.isFaceSturdy( + world, position, Direction.UP, SupportType.FULL)) { + return y; + } + } + return Integer.MIN_VALUE; + } + + public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area, + StructureStart start, BlockState expectedStilt, + IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null"); + Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver"); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + int baseColumns = 0; + int stiltBlocks = 0; + int stiltColumns = 0; + int unsupportedColumns = 0; + for (FoundationColumn column : foundationEnvelope(area, start)) { + int foundationY = findFoundationY(world, column, position); + if (foundationY == Integer.MIN_VALUE) { + continue; + } + baseColumns++; + int terrainY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))); + boolean grounded = foundationY <= terrainY + 1; + boolean stiltColumn = false; + for (int y = foundationY - 1; y >= area.minY(); y--) { + if (y <= terrainY) { + grounded = true; + break; + } + BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); + if (state.is(expectedStilt.getBlock())) { + stiltBlocks++; + stiltColumn = true; + if (y == area.minY()) { + grounded = true; + } + continue; + } + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + grounded = state.isSolid() && !vegetation; + break; + } + if (stiltColumn) { + stiltColumns++; + } + if (!grounded) { + unsupportedColumns++; + } + } + return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns); + } + + private static int findFoundationY(WorldGenLevel world, FoundationColumn column, + BlockPos.MutableBlockPos position) { + for (int cell = 0; cell < column.ys().length; cell++) { + int y = column.ys()[cell]; + BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); + if (state.isSolid()) { + return y; + } + } + return Integer.MIN_VALUE; + } + + private record FoundationColumn(int x, int z, int[] ys) { + } + + public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns, + int unsupportedColumns) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java index 8b8037f1f..60b0b4518 100644 --- a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java @@ -1,101 +1,22 @@ package art.arcane.iris.nativegen; -import art.arcane.iris.engine.framework.StructureVerticalBounds; -import art.arcane.iris.engine.mantle.components.StructureCarveEnvelope; -import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint; import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisNativeStructureDecision; -import art.arcane.iris.engine.object.IrisObjectVacuum; -import art.arcane.iris.engine.object.IrisStructureCarveShape; import art.arcane.iris.engine.object.IrisStructureStiltSettings; -import art.arcane.iris.engine.object.IrisStructureTerrain; -import art.arcane.iris.engine.object.IrisStructureTerrainMode; -import art.arcane.iris.engine.object.IrisStructureYBand; -import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.math.RNG; -import com.mojang.datafixers.util.Either; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.Identifier; -import net.minecraft.tags.BlockTags; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.StructureManager; import net.minecraft.world.level.WorldGenLevel; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.Rotation; -import net.minecraft.world.level.block.SupportType; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraft.world.level.chunk.LevelChunkSection; -import net.minecraft.world.level.levelgen.GenerationStep; import net.minecraft.world.level.levelgen.WorldgenRandom; import net.minecraft.world.level.levelgen.structure.BoundingBox; -import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; -import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; -import net.minecraft.world.level.levelgen.structure.StructurePiece; import net.minecraft.world.level.levelgen.structure.StructureStart; -import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; -import net.minecraft.world.level.levelgen.structure.Structure; -import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; -import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; -import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; -import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; -import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; -import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece; -import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.function.IntBinaryOperator; -import java.util.function.Supplier; public final class NativeStructurePostProcessor { - private static final int AUTO_ENCASE_PADDING = 3; - private static final long CARVE_CEILING_ROLL_SIGNATURE = 0x2A17L; - private static final long CARVE_FLOOR_ROLL_SIGNATURE = 0x5B3DL; - private static final long CARVE_LOBE_SIGNATURE = 0x7C41L; - private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid"; - private static final int FOUNDATION_VERTICAL_TOLERANCE = 1; - private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid"; - private static final int MAX_BURIAL_COLUMNS = 2_000_000; - private static final int MAX_CACHED_CARVE_FOOTPRINTS = 4; - private static final int MAX_CARVE_COLUMNS = 2_000_000; - private static final int MAX_TEMPLATE_OCCUPANCY_CELLS = 4_194_304; - private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24; - private static final String OCEAN_MONUMENT_ID = "minecraft:monument"; - private static final double SURFACE_TERRAIN_FALLOFF = 2.0; - private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L; - private static final int SURFACE_TERRAIN_RADIUS = 12; - private static final List TEMPLATE_VOID_BLOCKS = List.of(Blocks.AIR, Blocks.STRUCTURE_VOID); - private static final int UNDERGROUND_SURFACE_CLEARANCE = 1; - private static final Map CARVE_FOOTPRINTS = - Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75F, true) { - @Override - protected boolean removeEldestEntry( - Map.Entry eldest) { - return size() > MAX_CACHED_CARVE_FOOTPRINTS; - } - }); - private NativeStructurePostProcessor() { } @@ -105,1520 +26,30 @@ public final class NativeStructurePostProcessor { PaletteBlockResolver paletteBlockResolver, IntBinaryOperator surfaceHeight) { IrisStructureStiltSettings stilt = decision.stilt(); - ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(), generator.getSeaLevel(), - area.minY(), area.maxY() + 1); + NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(), + generator.getSeaLevel(), area.minY(), area.maxY() + 1); start.placeInChunk(world, structureManager, generator, random, area, chunkPos); if (stilt != null) { - placeStilts(world, area, structureId, start, stilt, paletteBlockResolver, surfaceHeight, - !isUndergroundStep(start.getStructure().step())); + NativeStructureFoundationBuilder.placeStilts(world, area, structureId, start, stilt, + paletteBlockResolver, surfaceHeight, + !NativeStructureVegetationClearer.isUndergroundStep(start.getStructure().step())); } } public static void prepareTerrain(WorldGenLevel world, BoundingBox area, - List targets, + List targets, PaletteBlockResolver paletteBlockResolver) { if (targets == null || targets.isEmpty()) { return; } - for (TerrainTarget target : targets) { - integrateTerrain(world, area, target.structureId(), target.start(), target.terrain(), - paletteBlockResolver); + for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { + NativeStructureTerrainIntegrator.integrateTerrain(world, area, target.structureId(), target.start(), + target.terrain(), paletteBlockResolver); } } - public static IrisStructureTerrain resolveNativeTerrain(StructureStart start, - IrisStructureTerrain configuredTerrain) { - if (configuredTerrain != null) { - return configuredTerrain; - } - if (start == null || !start.isValid() - || !encasesTerrain(start.getStructure().terrainAdaptation())) { - return null; - } - return new IrisStructureTerrain() - .setMode(IrisStructureTerrainMode.ENCASE) - .setHorizontalPadding(AUTO_ENCASE_PADDING) - .setCeilingPadding(AUTO_ENCASE_PADDING) - .setFloorPadding(AUTO_ENCASE_PADDING); - } - - static boolean encasesTerrain(TerrainAdjustment adjustment) { - return adjustment == TerrainAdjustment.BURY || adjustment == TerrainAdjustment.ENCAPSULATE; - } - - static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId, - StructureStart start, IrisStructureTerrain configuredTerrain, - PaletteBlockResolver paletteBlockResolver) { - IrisStructureTerrain terrain = configuredTerrain == null - ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) - : configuredTerrain; - IrisStructureTerrainMode mode = terrain.resolvedMode(); - if (mode == IrisStructureTerrainMode.SOURCE || mode == IrisStructureTerrainMode.PRESERVE) { - return; - } - if (mode == IrisStructureTerrainMode.VACUUM) { - carvePieceBoxes(world, area, start, terrain); - return; - } - if (mode == IrisStructureTerrainMode.ENCASE) { - encasePieces(world, area, structureId, start, terrain, paletteBlockResolver); - return; - } - if (mode != IrisStructureTerrainMode.BORE && mode != IrisStructureTerrainMode.FORCE_CARVE) { - throw new IllegalStateException("Native structure terrain mode " + mode - + " is not implemented for '" + structureId + "'"); - } - IrisStructureCarveShape shape = mode == IrisStructureTerrainMode.BORE - ? IrisStructureCarveShape.BOX : terrain.resolvedShape(); - if (shape == IrisStructureCarveShape.BOX) { - carvePieceBoxes(world, area, start, terrain); - return; - } - carveOrganicColumns(world, area, organicCarve( - carveFootprint(start, Math.max(0, terrain.getHorizontalPadding()), - () -> world.getLevel().getStructureManager()), - terrain, shape, carveNoiseIdentity(world, structureId, start))); - } - - static List contentPieceBounds(StructureStart start) { - List bounds = new ArrayList<>(start.getPieces().size()); - for (StructurePiece piece : start.getPieces()) { - if (!NativeStructureReferenceEnvelope.isMarker(piece)) { - bounds.add(piece.getBoundingBox()); - } - } - return List.copyOf(bounds); - } - - /** - * Per-column occupancy of the whole start, cached because a single structure spans many chunks and - * every one of them carves against the same shrinkwrapped footprint. - */ - static StructureCarvingFootprint carveFootprint(StructureStart start, int horizontalPadding, - Supplier templates) { - CarveFootprintKey key = new CarveFootprintKey(start, horizontalPadding); - StructureCarvingFootprint cached = CARVE_FOOTPRINTS.get(key); - if (cached != null) { - return cached; - } - StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns( - sink -> emitCarveColumns(start, templates, sink), horizontalPadding, MAX_CARVE_COLUMNS); - if (footprint == null) { - throw new IllegalStateException("Native structure carve footprint is empty or exceeds " - + MAX_CARVE_COLUMNS + " columns"); - } - CARVE_FOOTPRINTS.put(key, footprint); - return footprint; - } - - static OrganicCarve organicCarve(StructureCarvingFootprint footprint, IrisStructureTerrain terrain, - IrisStructureCarveShape shape, long identity) { - double strength = terrain.resolvedErosionStrength(); - double lobeStrength = terrain.resolvedLobeStrength(); - RNG noiseRng = new RNG(identity); - CNG blob = null; - CNG ceilingRoll = null; - CNG floorRoll = null; - CNG lobe = null; - if (shape == IrisStructureCarveShape.ERODED && strength > 0D) { - blob = CNG.signature(noiseRng); - ceilingRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_CEILING_ROLL_SIGNATURE)); - floorRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_FLOOR_ROLL_SIGNATURE)); - } - if (shape == IrisStructureCarveShape.ERODED && lobeStrength > 0D) { - // A plain single octave channel: the fractured signature noise has no usable low frequency band. - lobe = new CNG(noiseRng.nextParallelRNG(CARVE_LOBE_SIGNATURE), 1D, 1); - } - return new OrganicCarve(footprint, shape, - Math.max(0, terrain.getHorizontalPadding()), - Math.max(0, terrain.getCeilingPadding()), - Math.max(0, terrain.getFloorPadding()), - strength, terrain.resolvedErosionFrequency(), blob, ceilingRoll, floorRoll, - lobe, terrain.resolvedLobeFrequency(), lobeStrength); - } - - static void carveOrganicColumns(WorldGenLevel world, BoundingBox area, OrganicCarve carve) { - StructureCarvingFootprint footprint = carve.footprint(); - int minX = Math.max(area.minX(), footprint.minX()); - int maxX = Math.min(area.maxX(), footprint.maxX()); - int minZ = Math.max(area.minZ(), footprint.minZ()); - int maxZ = Math.min(area.maxZ(), footprint.maxZ()); - if (minX > maxX || minZ > maxZ) { - return; - } - boolean eroded = carve.blob() != null; - BlockState air = Blocks.AIR.defaultBlockState(); - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - for (int z = minZ; z <= maxZ; z++) { - for (int x = minX; x <= maxX; x++) { - int index = footprint.indexAt(x, z); - long horizontalDistanceSquared = footprint.distanceSquaredAt(index); - double sideReach = StructureCarveEnvelope.lobedSideReach(carve.lobe(), - carve.lobeFrequency(), carve.lobeStrength(), x, z, carve.horizontalPadding()); - double sideReachSquared = sideReach * sideReach; - if (horizontalDistanceSquared > sideReachSquared) { - continue; - } - double normalizedHorizontal = sideReachSquared == 0D - ? 0D : horizontalDistanceSquared / sideReachSquared; - int sourceMinY = footprint.sourceMinYAt(index); - int sourceMaxY = footprint.sourceMaxYAt(index); - double upReach = eroded - ? StructureCarveEnvelope.lobedUpReach(carve.lobe(), carve.lobeFrequency(), - carve.lobeStrength(), x, z, - StructureCarveEnvelope.erodedUpReach(carve.ceilingRoll(), - carve.frequency(), carve.strength(), x, z, - carve.ceilingPadding())) - : Math.max(1D, carve.ceilingPadding()); - double floorReach = eroded - ? StructureCarveEnvelope.erodedDownReach(carve.floorRoll(), carve.frequency(), - carve.strength(), x, z, carve.floorPadding()) - : carve.floorPadding(); - int columnMinY = Math.max(area.minY(), sourceMinY - (int) Math.floor(floorReach)); - int columnMaxY = Math.min(area.maxY(), - sourceMaxY + (eroded ? (int) Math.ceil(upReach) : carve.ceilingPadding())); - double downReach = Math.max(1D, floorReach); - for (int y = columnMinY; y <= columnMaxY; y++) { - double normalizedVertical = StructureCarveEnvelope.normalizedVerticalDistance( - y, sourceMinY, sourceMaxY, upReach, downReach); - double distanceSquared = normalizedHorizontal - + normalizedVertical * normalizedVertical; - if (distanceSquared > 1D) { - continue; - } - if (distanceSquared > 0D && eroded) { - double noise = carve.blob().fitDouble(0D, 1D, - x * carve.frequency(), y * carve.frequency(), z * carve.frequency()); - if (!StructureCarveEnvelope.shouldCarveOverboreCell( - carve.shape(), distanceSquared, noise, carve.strength())) { - continue; - } - } - world.setBlock(position.set(x, y, z), air, 2); - } - } - } - } - - private static boolean emitCarveColumns(StructureStart start, - Supplier templates, - StructureCarvingFootprint.ColumnSink sink) { - for (StructurePiece piece : start.getPieces()) { - if (NativeStructureReferenceEnvelope.isMarker(piece)) { - continue; - } - BoundingBox bounds = piece.getBoundingBox(); - if (!(piece instanceof PoolElementStructurePiece poolPiece) - || !emitTemplateColumns(pieceTemplates(poolPiece, templates), - poolPiece.getPosition(), poolPiece.getRotation(), bounds, sink)) { - emitBoxColumns(bounds, sink); - } - } - return true; - } - - /** - * Derives per-column vertical extents from the complement of the template's air and structure-void - * cells, so a carve tracks the actual silhouette instead of the piece's rectangular bounding box. - */ - static boolean emitTemplateColumns(List templates, BlockPos position, - Rotation rotation, BoundingBox bounds, - StructureCarvingFootprint.ColumnSink sink) { - if (templates.isEmpty()) { - return false; - } - int width = bounds.getXSpan(); - int depth = bounds.getZSpan(); - long cells = (long) width * depth * bounds.getYSpan(); - if (cells < 1L || cells > MAX_TEMPLATE_OCCUPANCY_CELLS) { - return false; - } - StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(rotation); - boolean[] voidCells = null; - for (StructureTemplate template : templates) { - boolean[] templateVoid = new boolean[(int) cells]; - for (Block ignored : TEMPLATE_VOID_BLOCKS) { - for (StructureTemplate.StructureBlockInfo info - : template.filterBlocks(position, settings, ignored)) { - BlockPos voidPosition = info.pos(); - if (bounds.isInside(voidPosition)) { - templateVoid[templateCellIndex(bounds, width, depth, - voidPosition.getX(), voidPosition.getY(), voidPosition.getZ())] = true; - } - } - } - if (voidCells == null) { - voidCells = templateVoid; - continue; - } - for (int cell = 0; cell < voidCells.length; cell++) { - voidCells[cell] &= templateVoid[cell]; - } - } - for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { - for (int x = bounds.minX(); x <= bounds.maxX(); x++) { - int minY = Integer.MAX_VALUE; - int maxY = Integer.MIN_VALUE; - for (int y = bounds.minY(); y <= bounds.maxY(); y++) { - if (voidCells[templateCellIndex(bounds, width, depth, x, y, z)]) { - continue; - } - if (minY == Integer.MAX_VALUE) { - minY = y; - } - maxY = y; - } - if (minY != Integer.MAX_VALUE) { - sink.column(x, z, minY, maxY); - } - } - } - return true; - } - - private static void emitBoxColumns(BoundingBox bounds, StructureCarvingFootprint.ColumnSink sink) { - for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { - for (int x = bounds.minX(); x <= bounds.maxX(); x++) { - sink.column(x, z, bounds.minY(), bounds.maxY()); - } - } - } - - private static int templateCellIndex(BoundingBox bounds, int width, int depth, - int x, int y, int z) { - return ((y - bounds.minY()) * depth + z - bounds.minZ()) * width + x - bounds.minX(); - } - - private static List pieceTemplates(PoolElementStructurePiece poolPiece, - Supplier templates) { - List resolved = new ArrayList<>(1); - collectElementTemplates(poolPiece.getElement(), templates, resolved); - return resolved; - } - - private static void collectElementTemplates(StructurePoolElement element, - Supplier templates, - List resolved) { - if (element instanceof ListPoolElement listElement) { - for (StructurePoolElement child : listElement.getElements()) { - collectElementTemplates(child, templates, resolved); - } - return; - } - if (element instanceof SinglePoolElement singleElement) { - resolved.add(resolveTemplate(singleElement, templates)); - } - } - - private static long carveNoiseIdentity(WorldGenLevel world, String structureId, - StructureStart start) { - return world.getSeed() - ^ ((long) start.getChunkPos().x() * 341873128712L) - ^ ((long) start.getChunkPos().z() * 132897987541L) - ^ (structureId == null ? 0 : structureId.hashCode()); - } - - private static void encasePieces(WorldGenLevel world, BoundingBox area, String structureId, - StructureStart start, IrisStructureTerrain terrain, - PaletteBlockResolver paletteBlockResolver) { - IrisMaterialPalette palette = terrain.getEncasePalette(); - RNG rng = null; - if (palette != null) { - Objects.requireNonNull(paletteBlockResolver, - "Native structure encase palette requires a platform block resolver"); - rng = new RNG(world.getSeed() ^ (structureId == null ? 0 : structureId.hashCode())); - } - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - for (BoundingBox bounds : contentPieceBounds(start)) { - BoundingBox shell = paddedPieceArea(area, bounds, terrain); - if (shell == null) { - continue; - } - for (int x = shell.minX(); x <= shell.maxX(); x++) { - for (int z = shell.minZ(); z <= shell.maxZ(); z++) { - for (int y = shell.minY(); y <= shell.maxY(); y++) { - BlockState existing = world.getBlockState(position.set(x, y, z)); - if (!isEncaseable(existing)) { - continue; - } - BlockState fill = palette == null - ? defaultEncaseBlock(y) - : Objects.requireNonNull( - paletteBlockResolver.resolve(palette, rng, x, y, z), - "Encase palette returned no block for " + structureId + " at " - + x + "," + y + "," + z); - world.setBlock(position, fill, 2); - } - } - } - } - } - - static boolean isEncaseable(BlockState state) { - return state.isAir() || !state.getFluidState().isEmpty(); - } - - static BlockState defaultEncaseBlock(int y) { - return y < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState(); - } - - private static void carvePieceBoxes(WorldGenLevel world, BoundingBox area, StructureStart start, - IrisStructureTerrain terrain) { - BlockState air = Blocks.AIR.defaultBlockState(); - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - for (BoundingBox bounds : contentPieceBounds(start)) { - BoundingBox carve = paddedPieceArea(area, bounds, terrain); - if (carve == null) { - continue; - } - for (int x = carve.minX(); x <= carve.maxX(); x++) { - for (int z = carve.minZ(); z <= carve.maxZ(); z++) { - for (int y = carve.minY(); y <= carve.maxY(); y++) { - world.setBlock(position.set(x, y, z), air, 2); - } - } - } - } - } - - private static BoundingBox paddedPieceArea(BoundingBox area, BoundingBox bounds, - IrisStructureTerrain terrain) { - int horizontalPadding = Math.max(0, terrain.getHorizontalPadding()); - int minX = Math.max(area.minX(), bounds.minX() - horizontalPadding); - int minY = Math.max(area.minY(), bounds.minY() - Math.max(0, terrain.getFloorPadding())); - int minZ = Math.max(area.minZ(), bounds.minZ() - horizontalPadding); - int maxX = Math.min(area.maxX(), bounds.maxX() + horizontalPadding); - int maxY = Math.min(area.maxY(), bounds.maxY() + Math.max(0, terrain.getCeilingPadding())); - int maxZ = Math.min(area.maxZ(), bounds.maxZ() + horizontalPadding); - if (minX > maxX || minY > maxY || minZ > maxZ) { - return null; - } - return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); - } - - public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset, - int seaLevel, int worldMinY, int worldMaxYExclusive, - boolean underground, boolean preserveSourceY, - IrisStructureYBand yBand, - IntBinaryOperator surfaceHeight) { - if (isOceanMonument(structureId)) { - return alignOceanMonumentToSeaLevel( - start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive); - } - if (isAdjustedScatteredStructure(structureId)) { - return alignScatteredStructureToSurface( - start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); - } - return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive, - underground, preserveSourceY, yBand, surfaceHeight); - } - - public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY, - LevelHeightAccessor heightAccessor) { - Objects.requireNonNull(start, "Native structure start must not be null"); - Objects.requireNonNull(source, "Native structure source must not be null"); - Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null"); - if (!start.isValid()) { - return StructureStart.INVALID_START; - } - List pieces = start.getPieces(); - int minY = Integer.MAX_VALUE; - for (StructurePiece piece : pieces) { - minY = Math.min(minY, piece.getBoundingBox().minY()); - } - if (minY == Integer.MAX_VALUE) { - return StructureStart.INVALID_START; - } - int offsetY = Math.subtractExact(targetMinY, minY); - if (offsetY != 0) { - for (StructurePiece piece : pieces) { - moveStructurePiece(piece, offsetY); - } - } - int worldMinY = heightAccessor.getMinY() + 1; - int worldMaxYExclusive = Math.addExact( - heightAccessor.getMinY(), heightAccessor.getHeight()); - for (StructurePiece piece : pieces) { - BoundingBox bounds = piece.getBoundingBox(); - if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) { - throw new IllegalStateException("Native structure cannot fit target minimum Y " - + targetMinY + " inside world bounds [" + worldMinY + "," - + worldMaxYExclusive + ")"); - } - } - return new StructureStart( - source, - start.getChunkPos(), - start.getReferences(), - new PiecesContainer(List.copyOf(pieces)) - ); - } - - static int alignScatteredStructureToSurface(StructureStart start, String structureId, - int configuredOffset, int worldMinY, - int worldMaxYExclusive, - IntBinaryOperator surfaceHeight) { - Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver"); - ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId); - BoundingBox bounds = start.getBoundingBox(); - BoundingBox pieceBounds = piece.getBoundingBox(); - int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight); - int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset); - int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY()); - int offsetY = StructureVerticalBounds.clampOffset( - bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive); - if (offsetY != 0) { - moveStructureStart(start, bounds, offsetY); - } - setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY())); - return offsetY; - } - - public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY, - int worldMaxYExclusive, boolean underground, - boolean preserveSourceY, IrisStructureYBand yBand, - IntBinaryOperator surfaceHeight) { - BoundingBox bounds = start.getBoundingBox(); - int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY, - worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight); - int offsetY = StructureVerticalBounds.clampOffset( - bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive); - if (offsetY == 0) { - return 0; - } - moveStructureStart(start, bounds, offsetY); - return offsetY; - } - - private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset, - int worldMinY, int worldMaxYExclusive, boolean underground, - boolean preserveSourceY, IrisStructureYBand yBand, - IntBinaryOperator surfaceHeight) { - if (preserveSourceY) { - return requestedOffset; - } - if (yBand != null) { - return resolveYBandOffset(bounds, yBand, start.getChunkPos()); - } - if (underground) { - return resolveBuriedOffset( - bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); - } - return requestedOffset; - } - - static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel, - int worldMinY, int worldMaxYExclusive) { - OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start); - BoundingBox bounds = start.getBoundingBox(); - int targetMinY = Math.addExact( - Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset); - int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY()); - int offsetY = StructureVerticalBounds.clampOffset( - bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive); - if (offsetY != requestedOffset) { - throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel - + " with configured offset " + configuredOffset + " inside world bounds [" - + worldMinY + "," + worldMaxYExclusive + ")"); - } - if (offsetY == 0) { - return 0; - } - moveStructureStart(start, bounds, offsetY); - return offsetY; - } - - private static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId, - int configuredOffset, int seaLevel, - int worldMinY, int worldMaxYExclusive) { - if (isOceanMonument(structureId)) { - alignOceanMonumentToSeaLevel( - start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive); - } - } - - private static boolean isOceanMonument(String structureId) { - return OCEAN_MONUMENT_ID.equals(structureId); - } - - private static boolean isAdjustedScatteredStructure(String structureId) { - return DESERT_PYRAMID_ID.equals(structureId) || JUNGLE_PYRAMID_ID.equals(structureId); - } - - private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start, - String structureId) { - Objects.requireNonNull(start, "Scattered native structure start must not be null"); - List pieces = start.getPieces(); - if (pieces.size() != 1) { - throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found " - + pieces.size()); - } - StructurePiece piece = pieces.get(0); - if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) { - return desertPyramid; - } - if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) { - return jungleTemple; - } - throw new IllegalStateException(structureId + " contains unexpected piece " - + piece.getClass().getName()); - } - - private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds, - IntBinaryOperator surfaceHeight) { - if (DESERT_PYRAMID_ID.equals(structureId)) { - return lowestSurfaceY(bounds, surfaceHeight); - } - if (JUNGLE_PYRAMID_ID.equals(structureId)) { - return averageSurfaceY(bounds, surfaceHeight); - } - throw new IllegalStateException("Unsupported scattered native structure " + structureId); - } - - private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { - int lowestY = Integer.MAX_VALUE; - for (int x = bounds.minX(); x <= bounds.maxX(); x++) { - for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { - lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z)); - } - } - if (lowestY == Integer.MAX_VALUE) { - throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); - } - return lowestY; - } - - private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { - long totalY = 0L; - long columns = 0L; - for (int x = bounds.minX(); x <= bounds.maxX(); x++) { - for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { - totalY += surfaceHeight.applyAsInt(x, z); - columns++; - } - } - if (columns == 0L) { - throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); - } - return Math.toIntExact(totalY / columns); - } - - private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) { - try { - ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition); - } catch (IllegalAccessException error) { - throw new IllegalStateException("Cannot lock scattered native structure height", error); - } - } - - static Field resolveScatteredHeightPositionField() { - Field resolved = null; - for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (Modifier.isStatic(modifiers) || field.getType() != int.class - || !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) { - continue; - } - if (resolved != null) { - throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields"); - } - resolved = field; - } - if (resolved == null) { - throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing"); - } - if (!resolved.trySetAccessible()) { - throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible"); - } - return resolved; - } - - private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) { - Objects.requireNonNull(start, "Ocean monument start must not be null"); - List pieces = start.getPieces(); - if (pieces.size() != 1 || !(pieces.get(0) instanceof OceanMonumentPieces.MonumentBuilding building)) { - throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found " - + pieces.size() + " top-level pieces"); - } - return building; - } - - private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) { - for (StructurePiece piece : start.getPieces()) { - moveStructurePiece(piece, offsetY); - } - cachedBounds.move(0, offsetY, 0); - } - - private static void moveStructurePiece(StructurePiece piece, int offsetY) { - piece.move(0, offsetY, 0); - if (piece instanceof OceanMonumentPieces.MonumentBuilding building) { - for (StructurePiece child : monumentChildPieces(building)) { - child.move(0, offsetY, 0); - } - } - if (piece instanceof PoolElementStructurePiece poolPiece) { - List junctions = poolPiece.getJunctions(); - for (int i = 0; i < junctions.size(); i++) { - JigsawJunction junction = junctions.get(i); - junctions.set(i, new JigsawJunction( - junction.getSourceX(), - junction.getSourceGroundY() + offsetY, - junction.getSourceZ(), - junction.getDeltaY(), - junction.getDestProjection())); - } - } - } - - static List monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) { - Object value; - try { - value = MonumentChildPiecesAccess.FIELD.get(building); - } catch (IllegalAccessException error) { - throw new IllegalStateException("Cannot read Ocean Monument child pieces", error); - } - if (!(value instanceof List children)) { - throw new IllegalStateException("Ocean Monument child-pieces field is not a list"); - } - List pieces = new ArrayList<>(children.size()); - for (Object child : children) { - if (!(child instanceof StructurePiece monumentPiece) - || child.getClass().getEnclosingClass() != OceanMonumentPieces.class) { - throw new IllegalStateException("Ocean Monument child-pieces list contains " - + (child == null ? "null" : child.getClass().getName())); - } - pieces.add(monumentPiece); - } - return List.copyOf(pieces); - } - - private static Field resolveMonumentChildPiecesField() { - Field resolved = null; - for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) { - if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) { - continue; - } - if (resolved != null) { - throw new IllegalStateException("Ocean Monument has multiple instance List fields"); - } - resolved = field; - } - if (resolved == null) { - throw new IllegalStateException("Ocean Monument child-pieces List field is missing"); - } - if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { - throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract"); - } - if (!resolved.trySetAccessible()) { - throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible"); - } - return resolved; - } - - static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) { - Objects.requireNonNull(bounds, "Native structure bounds must not be null"); - Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk"); - int target = yBandTargetMidpointY( - bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk); - return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY())); - } - - static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) { - int height = Math.subtractExact(maxY, minY); - int lowest = Math.addExact(bandMin, height / 2); - int highest = Math.subtractExact(bandMax, height - height / 2); - if (lowest > highest) { - // The band is shorter than the structure, so only its midpoint can be honoured. - return Math.floorDiv(Math.addExact(bandMin, bandMax), 2); - } - int span = highest - lowest + 1; - if (span == 1) { - return lowest; - } - long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L; - return lowest + new RNG(identity).nextInt(span); - } - - private static int midpointY(int minY, int maxY) { - return minY + (maxY - minY) / 2; - } - - static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY, - int worldMaxYExclusive, IntBinaryOperator surfaceHeight) { - Objects.requireNonNull(bounds, "Native structure bounds must not be null"); - Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver"); - long width = (long) bounds.maxX() - bounds.minX() + 1L; - long depth = (long) bounds.maxZ() - bounds.minZ() + 1L; - if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) { - throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds " - + MAX_BURIAL_COLUMNS + " columns"); - } - int maximumOffset = requestedOffset; - for (int x = bounds.minX(); x <= bounds.maxX(); x++) { - for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { - int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE; - maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY()); - } - } - int clampedOffset = StructureVerticalBounds.clampOffset( - bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive); - if (clampedOffset > maximumOffset) { - IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ() - + " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset); - } - return clampedOffset; - } - - public static boolean isUndergroundStep(GenerationStep.Decoration step) { - return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES - || step == GenerationStep.Decoration.UNDERGROUND_DECORATION - || step == GenerationStep.Decoration.STRONGHOLDS; - } - - public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step, - boolean configured) { - return configured; - } - - public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area, - List starts, - IntBinaryOperator surfaceHeight) { - if (starts == null || starts.isEmpty()) { - return; - } - Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver"); - List anchors = collectSurfaceAnchors(starts); - if (!anchors.isEmpty()) { - fitSurfaceTerrain(world, area, anchors, surfaceHeight); - } - Supplier templates = () -> world.getLevel().getStructureManager(); - for (StructureStart start : starts) { - if (requiresSurfaceTerrain(start)) { - clearLegacyTemplateAir(world, area, start, templates); - } - } - } - - static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment, - GenerationStep.Decoration step) { - return adjustment == TerrainAdjustment.BEARD_THIN - && step == GenerationStep.Decoration.SURFACE_STRUCTURES; - } - - static int resolveSurfaceTarget(List anchors, int worldX, int worldZ, - int originalY) { - int localTargetY = originalY; - SurfaceAnchor selectedLocal = null; - long totalInfluence = 0L; - long weightedMeetY = 0L; - long maximumInfluence = 0L; - for (SurfaceAnchor anchor : anchors) { - int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX()); - int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ()); - long distanceSquared = (long) outX * outX + (long) outZ * outZ; - if (distanceSquared > (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS) { - continue; - } - boolean containsColumn = outX == 0 && outZ == 0; - if (containsColumn) { - if (precedes(anchor, selectedLocal)) { - localTargetY = anchor.meetY(); - selectedLocal = anchor; - } - continue; - } - double factor = IrisObjectVacuum.columnInfluence( - worldX, worldZ, - anchor.minX(), anchor.maxX(), anchor.minZ(), anchor.maxZ(), - SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF); - long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE); - if (influence <= 0L) { - continue; - } - long weightedInfluence = influence * Math.max(1, anchor.strength()); - totalInfluence += weightedInfluence; - weightedMeetY += weightedInfluence * anchor.meetY(); - maximumInfluence = Math.max(maximumInfluence, influence); - } - if (selectedLocal != null) { - return localTargetY; - } - if (totalInfluence == 0L) { - return originalY; - } - double blendedMeetY = weightedMeetY / (double) totalInfluence; - double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE; - return (int) Math.round(originalY + ((blendedMeetY - originalY) * factor)); - } - - private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) { - if (selected == null) { - return true; - } - if (candidate.strength() != selected.strength()) { - return candidate.strength() > selected.strength(); - } - if (candidate.meetY() != selected.meetY()) { - return candidate.meetY() < selected.meetY(); - } - if (candidate.minX() != selected.minX()) { - return candidate.minX() < selected.minX(); - } - if (candidate.minZ() != selected.minZ()) { - return candidate.minZ() < selected.minZ(); - } - if (candidate.maxX() != selected.maxX()) { - return candidate.maxX() < selected.maxX(); - } - if (candidate.maxZ() != selected.maxZ()) { - return candidate.maxZ() < selected.maxZ(); - } - return false; - } - - private static List collectSurfaceAnchors(List starts) { - List anchors = new ArrayList<>(); - for (StructureStart start : starts) { - if (!requiresSurfaceTerrain(start)) { - continue; - } - for (StructurePiece piece : start.getPieces()) { - if (NativeStructureReferenceEnvelope.isMarker(piece)) { - continue; - } - if (piece instanceof PoolElementStructurePiece poolPiece) { - if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) { - BoundingBox bounds = poolPiece.getBoundingBox(); - anchors.add(new SurfaceAnchor( - bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), - bounds.minY() + poolPiece.getGroundLevelDelta() - 1, 2)); - } - for (JigsawJunction junction : poolPiece.getJunctions()) { - anchors.add(new SurfaceAnchor( - junction.getSourceX(), junction.getSourceX(), - junction.getSourceZ(), junction.getSourceZ(), - junction.getSourceGroundY() - 1, 1)); - } - continue; - } - BoundingBox bounds = piece.getBoundingBox(); - anchors.add(new SurfaceAnchor( - bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), - bounds.minY() - 1, 2)); - } - } - return List.copyOf(anchors); - } - - static boolean requiresSurfaceTerrain(StructureStart start) { - return start != null - && start.isValid() - && shouldPrepareSurfaceTerrain( - start.getStructure().terrainAdaptation(), start.getStructure().step()); - } - - private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area, - List anchors, - IntBinaryOperator surfaceHeight) { - int width = area.getXSpan(); - int depth = area.getZSpan(); - int[] originalHeights = new int[width * depth]; - int[] targetHeights = new int[width * depth]; - for (int z = area.minZ(); z <= area.maxZ(); z++) { - for (int x = area.minX(); x <= area.maxX(); x++) { - int column = (z - area.minZ()) * width + x - area.minX(); - int originalY = Math.max(area.minY(), Math.min( - area.maxY(), surfaceHeight.applyAsInt(x, z))); - originalHeights[column] = originalY; - targetHeights[column] = Math.max(area.minY(), Math.min( - area.maxY(), resolveSurfaceTarget(anchors, x, z, originalY))); - } - } - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - for (int z = area.minZ(); z <= area.maxZ(); z++) { - for (int x = area.minX(); x <= area.maxX(); x++) { - int column = (z - area.minZ()) * width + x - area.minX(); - applySurfaceColumn(world, position, x, z, - originalHeights[column], targetHeights[column], area.minY(), area.maxY()); - } - } - } - - static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position, - int x, int z, int originalY, int targetY, - int worldMinY, int worldMaxY) { - if (targetY == originalY) { - return; - } - SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY); - if (targetY < originalY) { - BlockState clearedState = clearSurfaceDecorationAndResolveFill( - world, position, x, z, originalY, worldMaxY); - for (int y = originalY; y > targetY; y--) { - world.setBlock(position.set(x, y, z), clearedState, 2); - } - world.setBlock(position.set(x, targetY, z), materials.surface(), 2); - return; - } - for (int y = originalY + 1; y < targetY; y++) { - position.set(x, y, z); - if (!isTreeBlock(world.getBlockState(position))) { - world.setBlock(position, materials.subsurface(), 2); - } - } - position.set(x, targetY, z); - if (!isTreeBlock(world.getBlockState(position))) { - world.setBlock(position, materials.surface(), 2); - } - } - - private static BlockState clearSurfaceDecorationAndResolveFill( - WorldGenLevel world, BlockPos.MutableBlockPos position, - int x, int z, int originalY, int worldMaxY) { - BlockState air = Blocks.AIR.defaultBlockState(); - for (int y = originalY + 1; y <= worldMaxY; y++) { - BlockState state = world.getBlockState(position.set(x, y, z)); - if (state.isAir()) { - return air; - } - if (!state.getFluidState().isEmpty()) { - BlockState fluid = state.getFluidState().createLegacyBlock(); - if (state != fluid) { - world.setBlock(position, fluid, 2); - } - return fluid; - } - if (state.isSolid() || isTreeBlock(state)) { - return air; - } - world.setBlock(position, air, 2); - } - return air; - } - - private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world, - BlockPos.MutableBlockPos position, - int x, int z, int originalY, - int worldMinY) { - BlockState surface = world.getBlockState(position.set(x, originalY, z)); - BlockState subsurface = null; - for (int y = originalY - 1; y >= worldMinY; y--) { - BlockState candidate = world.getBlockState(position.set(x, y, z)); - if (isTerrainBlock(candidate)) { - subsurface = candidate; - break; - } - } - if (subsurface == null) { - subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState(); - } - if (!isTerrainBlock(surface)) { - surface = subsurface; - } - return new SurfaceMaterials(surface, subsurface); - } - - private static boolean isTerrainBlock(BlockState state) { - return state.isSolid() && !isTreeBlock(state); - } - - private static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area, - StructureStart start, - Supplier templates) { - for (StructurePiece piece : start.getPieces()) { - if (NativeStructureReferenceEnvelope.isMarker(piece)) { - continue; - } - if (!(piece instanceof PoolElementStructurePiece poolPiece) - || poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID - || !intersects(poolPiece.getBoundingBox(), area)) { - continue; - } - int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta(); - StructurePlaceSettings settings = new StructurePlaceSettings() - .setRotation(poolPiece.getRotation()) - .setBoundingBox(area); - clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(), - groundY, settings, templates); - } - } - - private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element, - BlockPos position, int groundY, - StructurePlaceSettings settings, - Supplier templates) { - if (element instanceof ListPoolElement listElement) { - for (StructurePoolElement child : listElement.getElements()) { - clearLegacyTemplateAir(world, child, position, groundY, settings, templates); - } - return; - } - if (!(element instanceof LegacySinglePoolElement legacyElement)) { - return; - } - StructureTemplate template = resolveTemplate(legacyElement, templates); - clearTemplateAir(world, template, position, groundY, settings); - } - - static void clearTemplateAir(WorldGenLevel world, StructureTemplate template, - BlockPos position, int groundY, - StructurePlaceSettings settings) { - List airBlocks = template.filterBlocks( - position, settings, Blocks.AIR); - BlockState air = Blocks.AIR.defaultBlockState(); - for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) { - BlockPos airPosition = airBlock.pos(); - BlockState existingState = world.getBlockState(airPosition); - if (shouldClearLegacyAir( - airPosition.getY(), groundY, existingState.isAir()) - && !isTreeBlock(existingState)) { - world.setBlock(airPosition, air, 2); - } - } - } - - static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) { - return airY >= groundY && !existingAir; - } - - static boolean intersects(BoundingBox first, BoundingBox second) { - return first.maxX() >= second.minX() && first.minX() <= second.maxX() - && first.maxY() >= second.minY() && first.minY() <= second.maxY() - && first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ(); - } - - private static StructureTemplate resolveTemplate(SinglePoolElement element, - Supplier templates) { - Object value; - try { - value = SinglePoolTemplateAccess.FIELD.get(element); - } catch (IllegalAccessException error) { - throw new IllegalStateException("Cannot read native structure pool template", error); - } - if (!(value instanceof Either reference)) { - throw new IllegalStateException("Native structure pool template field is not an Either"); - } - return resolveTemplateReference(reference, templates); - } - - static StructureTemplate resolveTemplateReference(Either reference, - Supplier templates) { - return reference.map( - location -> resolveNamedTemplate(location, templates), - NativeStructurePostProcessor::requireRuntimeTemplate); - } - - private static StructureTemplate resolveNamedTemplate(Object value, - Supplier templates) { - if (!(value instanceof Identifier identifier)) { - throw new IllegalStateException("Native structure pool template identifier is " - + (value == null ? "null" : value.getClass().getName())); - } - return Objects.requireNonNull(templates == null ? null : templates.get(), - "Native structure template manager is unavailable").getOrCreate(identifier); - } - - private static StructureTemplate requireRuntimeTemplate(Object value) { - if (!(value instanceof StructureTemplate template)) { - throw new IllegalStateException("Native runtime structure pool template is " - + (value == null ? "null" : value.getClass().getName())); - } - return template; - } - - static Field resolveSinglePoolTemplateField() { - Field resolved = null; - for (Field field : SinglePoolElement.class.getDeclaredFields()) { - if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) { - continue; - } - if (resolved != null) { - throw new IllegalStateException("SinglePoolElement has multiple instance Either fields"); - } - resolved = field; - } - if (resolved == null) { - throw new IllegalStateException("SinglePoolElement template Either field is missing"); - } - if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { - throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract"); - } - if (!resolved.trySetAccessible()) { - throw new IllegalStateException("SinglePoolElement template field is inaccessible"); - } - return resolved; - } - - public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area, - List targets) { - if (targets == null || targets.isEmpty()) { - return; - } - VegetationSnapshot snapshot = captureVegetation(chunk, area); - if (snapshot.treeBlockCount() == 0) { - return; - } - boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()]; - for (VegetationTarget target : targets) { - if (target != null && target.force() && target.start() != null && target.start().isValid()) { - markVegetationColumns(area, snapshot, target, clearColumns); - } - } - clearVegetationColumns(world, area, snapshot, clearColumns); - } - - private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) { - int width = area.getXSpan(); - int depth = area.getZSpan(); - BitSet[] columns = new BitSet[width * depth]; - int[] lowestY = new int[columns.length]; - Arrays.fill(lowestY, Integer.MAX_VALUE); - int treeBlockCount = 0; - LevelChunkSection[] sections = chunk.getSections(); - int chunkMinX = chunk.getPos().getMinBlockX(); - int chunkMinZ = chunk.getPos().getMinBlockZ(); - for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { - LevelChunkSection section = sections[sectionIndex]; - int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; - int minY = Math.max(area.minY(), sectionMinY); - int maxY = Math.min(area.maxY(), sectionMinY + 15); - if (minY > maxY || section.hasOnlyAir() || !section.maybeHas(NativeStructurePostProcessor::isTreeBlock)) { - continue; - } - for (int y = minY; y <= maxY; y++) { - int localY = y - sectionMinY; - for (int z = area.minZ(); z <= area.maxZ(); z++) { - int localZ = z - chunkMinZ; - for (int x = area.minX(); x <= area.maxX(); x++) { - int localX = x - chunkMinX; - if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) { - continue; - } - int column = (z - area.minZ()) * width + x - area.minX(); - BitSet treeBlocks = columns[column]; - if (treeBlocks == null) { - treeBlocks = new BitSet(area.getYSpan()); - columns[column] = treeBlocks; - } - treeBlocks.set(y - area.minY()); - lowestY[column] = Math.min(lowestY[column], y); - treeBlockCount++; - } - } - } - } - return new VegetationSnapshot(columns, lowestY, treeBlockCount); - } - - private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot, - VegetationTarget target, boolean[] clearColumns) { - int width = area.getXSpan(); - int[] pieceTops = new int[clearColumns.length]; - Arrays.fill(pieceTops, Integer.MIN_VALUE); - for (StructurePiece piece : target.start().getPieces()) { - if (NativeStructureReferenceEnvelope.isMarker(piece)) { - continue; - } - BoundingBox bounds = piece.getBoundingBox(); - int minX = Math.max(area.minX(), bounds.minX()); - int maxX = Math.min(area.maxX(), bounds.maxX()); - int minZ = Math.max(area.minZ(), bounds.minZ()); - int maxZ = Math.min(area.maxZ(), bounds.maxZ()); - if (minX > maxX || minZ > maxZ) { - continue; - } - for (int z = minZ; z <= maxZ; z++) { - for (int x = minX; x <= maxX; x++) { - int column = (z - area.minZ()) * width + x - area.minX(); - pieceTops[column] = Math.max(pieceTops[column], bounds.maxY()); - } - } - } - for (int column = 0; column < pieceTops.length; column++) { - if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) { - continue; - } - if (shouldClearVegetationColumn(pieceTops[column], snapshot.lowestY()[column], target.force())) { - clearColumns[column] = true; - } - } - } - - static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) { - return force || pieceTopY >= lowestTreeY; - } - - private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area, - VegetationSnapshot snapshot, boolean[] clearColumns) { - int width = area.getXSpan(); - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - BlockState air = Blocks.AIR.defaultBlockState(); - for (int z = area.minZ(); z <= area.maxZ(); z++) { - for (int x = area.minX(); x <= area.maxX(); x++) { - int column = (z - area.minZ()) * width + x - area.minX(); - BitSet treeBlocks = snapshot.columns()[column]; - if (!clearColumns[column] || treeBlocks == null) { - continue; - } - for (int bit = treeBlocks.nextSetBit(0); bit >= 0; bit = treeBlocks.nextSetBit(bit + 1)) { - int y = area.minY() + bit; - position.set(x, y, z); - BlockState state = world.getBlockState(position); - if (isTreeBlock(state)) { - world.setBlock(position, air, 2); - } - } - } - } - } - - private static boolean isTreeBlock(BlockState state) { - if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) { - return true; - } - String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath(); - return path.endsWith("_log") || path.endsWith("_wood") - || path.endsWith("_stem") || path.endsWith("_hyphae") - || path.endsWith("_leaves"); - } - - private static List foundationEnvelope(BoundingBox area, StructureStart start) { - BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start); - int minX = Math.max(area.minX(), structure.minX()); - int minZ = Math.max(area.minZ(), structure.minZ()); - int maxX = Math.min(area.maxX(), structure.maxX()); - int maxZ = Math.min(area.maxZ(), structure.maxZ()); - if (minX > maxX || minZ > maxZ) { - return List.of(); - } - List pieces = start.getPieces(); - List columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1)); - BitSet envelope = new BitSet(area.getYSpan()); - for (int z = minZ; z <= maxZ; z++) { - for (int x = minX; x <= maxX; x++) { - envelope.clear(); - markFoundationEnvelope(envelope, pieces, area, x, z); - int cellCount = envelope.cardinality(); - if (cellCount == 0) { - continue; - } - int[] ys = new int[cellCount]; - int cell = 0; - for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) { - ys[cell++] = area.minY() + bit; - } - columns.add(new FoundationColumn(x, z, ys)); - } - } - return List.copyOf(columns); - } - - private static void markFoundationEnvelope(BitSet envelope, List pieces, BoundingBox area, - int x, int z) { - for (StructurePiece piece : pieces) { - if (NativeStructureReferenceEnvelope.isMarker(piece)) { - continue; - } - BoundingBox bounds = piece.getBoundingBox(); - if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) { - continue; - } - int groundY = bounds.minY(); - if (piece instanceof PoolElementStructurePiece poolPiece) { - groundY += poolPiece.getGroundLevelDelta(); - if (groundY < bounds.minY()) { - continue; - } - } - int minY = Math.max(area.minY(), bounds.minY()); - int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE)); - if (minY <= maxY) { - envelope.set(minY - area.minY(), maxY - area.minY() + 1); - } - } - } - - private static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId, - StructureStart start, IrisStructureStiltSettings settings, - PaletteBlockResolver paletteBlockResolver, - IntBinaryOperator surfaceHeight, - boolean surfaceStructure) { - Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver"); - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - int structureHash = structureId == null ? 0 : structureId.hashCode(); - RNG rng = new RNG(world.getSeed() ^ structureHash); - for (FoundationColumn column : foundationEnvelope(area, start)) { - if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) { - continue; - } - int foundationY = findFoundationY(world, column, position); - if (foundationY == Integer.MIN_VALUE) { - continue; - } - int terrainY = surfaceStructure - ? Math.max(area.minY(), Math.min( - area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))) - : area.minY() - 1; - int anchorY = findStiltAnchorY( - world, column.x(), column.z(), foundationY, - Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position); - if (anchorY == Integer.MIN_VALUE) { - continue; - } - for (int y = foundationY - 1; y > anchorY; y--) { - position.set(column.x(), y, column.z()); - BlockState stilt = settings.getPalette() == null - ? Blocks.COBBLESTONE.defaultBlockState() - : Objects.requireNonNull( - paletteBlockResolver.resolve( - settings.getPalette(), rng, column.x(), y, column.z()), - "Stilt palette returned no block for " + structureId + " at " - + column.x() + "," + y + "," + column.z()); - world.setBlock(position, stilt, 2); - } - } - } - - static boolean isStiltColumn(int x, int z, int spacing) { - int resolvedSpacing = Math.max(1, spacing); - return resolvedSpacing == 1 - || Math.floorMod(x, resolvedSpacing) == 0 - && Math.floorMod(z, resolvedSpacing) == 0; - } - - static int findStiltAnchorY( - WorldGenLevel world, int x, int z, int foundationY, int maxDepth, - int terrainY, int areaMinY, BlockPos.MutableBlockPos position) { - int minimumAnchorY = Math.max( - areaMinY, Math.max(terrainY, foundationY - maxDepth - 1)); - for (int y = foundationY - 1; y >= minimumAnchorY; y--) { - BlockState state = world.getBlockState(position.set(x, y, z)); - boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); - if (!vegetation && state.isFaceSturdy( - world, position, Direction.UP, SupportType.FULL)) { - return y; - } - } - return Integer.MIN_VALUE; - } - - public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area, - StructureStart start, BlockState expectedStilt, - IntBinaryOperator surfaceHeight) { - Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null"); - Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver"); - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - int baseColumns = 0; - int stiltBlocks = 0; - int stiltColumns = 0; - int unsupportedColumns = 0; - for (FoundationColumn column : foundationEnvelope(area, start)) { - int foundationY = findFoundationY(world, column, position); - if (foundationY == Integer.MIN_VALUE) { - continue; - } - baseColumns++; - int terrainY = Math.max(area.minY(), Math.min( - area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z()))); - boolean grounded = foundationY <= terrainY + 1; - boolean stiltColumn = false; - for (int y = foundationY - 1; y >= area.minY(); y--) { - if (y <= terrainY) { - grounded = true; - break; - } - BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); - if (state.is(expectedStilt.getBlock())) { - stiltBlocks++; - stiltColumn = true; - if (y == area.minY()) { - grounded = true; - } - continue; - } - boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); - grounded = state.isSolid() && !vegetation; - break; - } - if (stiltColumn) { - stiltColumns++; - } - if (!grounded) { - unsupportedColumns++; - } - } - return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns); - } - - private static int findFoundationY(WorldGenLevel world, FoundationColumn column, - BlockPos.MutableBlockPos position) { - for (int cell = 0; cell < column.ys().length; cell++) { - int y = column.ys()[cell]; - BlockState state = world.getBlockState(position.set(column.x(), y, column.z())); - if (state.isSolid()) { - return y; - } - } - return Integer.MIN_VALUE; - } - @FunctionalInterface public interface PaletteBlockResolver { BlockState resolve(IrisMaterialPalette palette, RNG rng, int x, int y, int z); } - - private record FoundationColumn(int x, int z, int[] ys) { - } - - // StructureStart has no value equality, so the key pins the exact start instance a chunk carves against. - private record CarveFootprintKey(StructureStart start, int padding) { - } - - record OrganicCarve(StructureCarvingFootprint footprint, IrisStructureCarveShape shape, - int horizontalPadding, int ceilingPadding, int floorPadding, - double strength, double frequency, CNG blob, CNG ceilingRoll, CNG floorRoll, - CNG lobe, double lobeFrequency, double lobeStrength) { - } - - public record VegetationTarget(StructureStart start, boolean force) { - } - - public record TerrainTarget(String structureId, StructureStart start, - IrisStructureTerrain terrain) { - } - - public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns, - int unsupportedColumns) { - } - - private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) { - } - - record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) { - } - - private record SurfaceMaterials(BlockState surface, BlockState subsurface) { - } - - private static final class MonumentChildPiecesAccess { - private static final Field FIELD = resolveMonumentChildPiecesField(); - - private MonumentChildPiecesAccess() { - } - } - - private static final class ScatteredHeightPositionAccess { - private static final Field FIELD = resolveScatteredHeightPositionField(); - - private ScatteredHeightPositionAccess() { - } - } - - private static final class SinglePoolTemplateAccess { - private static final Field FIELD = resolveSinglePoolTemplateField(); - - private SinglePoolTemplateAccess() { - } - } } diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java new file mode 100644 index 000000000..2b78953f4 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureReflection.java @@ -0,0 +1,148 @@ +package art.arcane.iris.nativegen; + +import com.mojang.datafixers.util.Either; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +final class NativeStructureReflection { + private NativeStructureReflection() { + } + + static Field resolveScatteredHeightPositionField() { + Field resolved = null; + for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || field.getType() != int.class + || !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible"); + } + return resolved; + } + + private static Field resolveMonumentChildPiecesField() { + Field resolved = null; + for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("Ocean Monument has multiple instance List fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("Ocean Monument child-pieces List field is missing"); + } + if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible"); + } + return resolved; + } + + static Field resolveSinglePoolTemplateField() { + Field resolved = null; + for (Field field : SinglePoolElement.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) { + continue; + } + if (resolved != null) { + throw new IllegalStateException("SinglePoolElement has multiple instance Either fields"); + } + resolved = field; + } + if (resolved == null) { + throw new IllegalStateException("SinglePoolElement template Either field is missing"); + } + if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) { + throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract"); + } + if (!resolved.trySetAccessible()) { + throw new IllegalStateException("SinglePoolElement template field is inaccessible"); + } + return resolved; + } + + static StructureTemplate resolveTemplate(SinglePoolElement element, + Supplier templates) { + Object value; + try { + value = SinglePoolTemplateAccess.FIELD.get(element); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read native structure pool template", error); + } + if (!(value instanceof Either reference)) { + throw new IllegalStateException("Native structure pool template field is not an Either"); + } + return resolveTemplateReference(reference, templates); + } + + static StructureTemplate resolveTemplateReference(Either reference, + Supplier templates) { + return reference.map( + location -> resolveNamedTemplate(location, templates), + NativeStructureReflection::requireRuntimeTemplate); + } + + private static StructureTemplate resolveNamedTemplate(Object value, + Supplier templates) { + if (!(value instanceof Identifier identifier)) { + throw new IllegalStateException("Native structure pool template identifier is " + + (value == null ? "null" : value.getClass().getName())); + } + return Objects.requireNonNull(templates == null ? null : templates.get(), + "Native structure template manager is unavailable").getOrCreate(identifier); + } + + private static StructureTemplate requireRuntimeTemplate(Object value) { + if (!(value instanceof StructureTemplate template)) { + throw new IllegalStateException("Native runtime structure pool template is " + + (value == null ? "null" : value.getClass().getName())); + } + return template; + } + + static final class MonumentChildPiecesAccess { + static final Field FIELD = resolveMonumentChildPiecesField(); + + private MonumentChildPiecesAccess() { + } + } + + static final class ScatteredHeightPositionAccess { + static final Field FIELD = resolveScatteredHeightPositionField(); + + private ScatteredHeightPositionAccess() { + } + } + + private static final class SinglePoolTemplateAccess { + private static final Field FIELD = resolveSinglePoolTemplateField(); + + private SinglePoolTemplateAccess() { + } + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java new file mode 100644 index 000000000..07b2804b6 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureSurfaceFitter.java @@ -0,0 +1,279 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.object.IrisObjectVacuum; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; +import java.util.function.Supplier; + +public final class NativeStructureSurfaceFitter { + private static final double SURFACE_TERRAIN_FALLOFF = 2.0; + private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L; + private static final int SURFACE_TERRAIN_RADIUS = 12; + + private NativeStructureSurfaceFitter() { + } + + public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area, + List starts, + IntBinaryOperator surfaceHeight) { + if (starts == null || starts.isEmpty()) { + return; + } + Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver"); + List anchors = collectSurfaceAnchors(starts); + if (!anchors.isEmpty()) { + fitSurfaceTerrain(world, area, anchors, surfaceHeight); + } + Supplier templates = () -> world.getLevel().getStructureManager(); + for (StructureStart start : starts) { + if (requiresSurfaceTerrain(start)) { + NativeStructureTerrainIntegrator.clearLegacyTemplateAir(world, area, start, templates); + } + } + } + + static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment, + GenerationStep.Decoration step) { + return adjustment == TerrainAdjustment.BEARD_THIN + && step == GenerationStep.Decoration.SURFACE_STRUCTURES; + } + + static int resolveSurfaceTarget(List anchors, int worldX, int worldZ, + int originalY) { + int localTargetY = originalY; + SurfaceAnchor selectedLocal = null; + long totalInfluence = 0L; + long weightedMeetY = 0L; + long maximumInfluence = 0L; + for (SurfaceAnchor anchor : anchors) { + int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX()); + int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ()); + long distanceSquared = (long) outX * outX + (long) outZ * outZ; + if (distanceSquared > (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS) { + continue; + } + boolean containsColumn = outX == 0 && outZ == 0; + if (containsColumn) { + if (precedes(anchor, selectedLocal)) { + localTargetY = anchor.meetY(); + selectedLocal = anchor; + } + continue; + } + double factor = IrisObjectVacuum.columnInfluence( + worldX, worldZ, + anchor.minX(), anchor.maxX(), anchor.minZ(), anchor.maxZ(), + SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF); + long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE); + if (influence <= 0L) { + continue; + } + long weightedInfluence = influence * Math.max(1, anchor.strength()); + totalInfluence += weightedInfluence; + weightedMeetY += weightedInfluence * anchor.meetY(); + maximumInfluence = Math.max(maximumInfluence, influence); + } + if (selectedLocal != null) { + return localTargetY; + } + if (totalInfluence == 0L) { + return originalY; + } + double blendedMeetY = weightedMeetY / (double) totalInfluence; + double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE; + return (int) Math.round(originalY + ((blendedMeetY - originalY) * factor)); + } + + private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) { + if (selected == null) { + return true; + } + if (candidate.strength() != selected.strength()) { + return candidate.strength() > selected.strength(); + } + if (candidate.meetY() != selected.meetY()) { + return candidate.meetY() < selected.meetY(); + } + if (candidate.minX() != selected.minX()) { + return candidate.minX() < selected.minX(); + } + if (candidate.minZ() != selected.minZ()) { + return candidate.minZ() < selected.minZ(); + } + if (candidate.maxX() != selected.maxX()) { + return candidate.maxX() < selected.maxX(); + } + if (candidate.maxZ() != selected.maxZ()) { + return candidate.maxZ() < selected.maxZ(); + } + return false; + } + + private static List collectSurfaceAnchors(List starts) { + List anchors = new ArrayList<>(); + for (StructureStart start : starts) { + if (!requiresSurfaceTerrain(start)) { + continue; + } + for (StructurePiece piece : start.getPieces()) { + if (NativeStructureReferenceEnvelope.isMarker(piece)) { + continue; + } + if (piece instanceof PoolElementStructurePiece poolPiece) { + if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) { + BoundingBox bounds = poolPiece.getBoundingBox(); + anchors.add(new SurfaceAnchor( + bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), + bounds.minY() + poolPiece.getGroundLevelDelta() - 1, 2)); + } + for (JigsawJunction junction : poolPiece.getJunctions()) { + anchors.add(new SurfaceAnchor( + junction.getSourceX(), junction.getSourceX(), + junction.getSourceZ(), junction.getSourceZ(), + junction.getSourceGroundY() - 1, 1)); + } + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + anchors.add(new SurfaceAnchor( + bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(), + bounds.minY() - 1, 2)); + } + } + return List.copyOf(anchors); + } + + static boolean requiresSurfaceTerrain(StructureStart start) { + return start != null + && start.isValid() + && shouldPrepareSurfaceTerrain( + start.getStructure().terrainAdaptation(), start.getStructure().step()); + } + + private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area, + List anchors, + IntBinaryOperator surfaceHeight) { + int width = area.getXSpan(); + int depth = area.getZSpan(); + int[] originalHeights = new int[width * depth]; + int[] targetHeights = new int[width * depth]; + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + int originalY = Math.max(area.minY(), Math.min( + area.maxY(), surfaceHeight.applyAsInt(x, z))); + originalHeights[column] = originalY; + targetHeights[column] = Math.max(area.minY(), Math.min( + area.maxY(), resolveSurfaceTarget(anchors, x, z, originalY))); + } + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + applySurfaceColumn(world, position, x, z, + originalHeights[column], targetHeights[column], area.minY(), area.maxY()); + } + } + } + + static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int targetY, + int worldMinY, int worldMaxY) { + if (targetY == originalY) { + return; + } + SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY); + if (targetY < originalY) { + BlockState clearedState = clearSurfaceDecorationAndResolveFill( + world, position, x, z, originalY, worldMaxY); + for (int y = originalY; y > targetY; y--) { + world.setBlock(position.set(x, y, z), clearedState, 2); + } + world.setBlock(position.set(x, targetY, z), materials.surface(), 2); + return; + } + for (int y = originalY + 1; y < targetY; y++) { + position.set(x, y, z); + if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.subsurface(), 2); + } + } + position.set(x, targetY, z); + if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.surface(), 2); + } + } + + private static BlockState clearSurfaceDecorationAndResolveFill( + WorldGenLevel world, BlockPos.MutableBlockPos position, + int x, int z, int originalY, int worldMaxY) { + BlockState air = Blocks.AIR.defaultBlockState(); + for (int y = originalY + 1; y <= worldMaxY; y++) { + BlockState state = world.getBlockState(position.set(x, y, z)); + if (state.isAir()) { + return air; + } + if (!state.getFluidState().isEmpty()) { + BlockState fluid = state.getFluidState().createLegacyBlock(); + if (state != fluid) { + world.setBlock(position, fluid, 2); + } + return fluid; + } + if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)) { + return air; + } + world.setBlock(position, air, 2); + } + return air; + } + + private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world, + BlockPos.MutableBlockPos position, + int x, int z, int originalY, + int worldMinY) { + BlockState surface = world.getBlockState(position.set(x, originalY, z)); + BlockState subsurface = null; + for (int y = originalY - 1; y >= worldMinY; y--) { + BlockState candidate = world.getBlockState(position.set(x, y, z)); + if (isTerrainBlock(candidate)) { + subsurface = candidate; + break; + } + } + if (subsurface == null) { + subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState(); + } + if (!isTerrainBlock(surface)) { + surface = subsurface; + } + return new SurfaceMaterials(surface, subsurface); + } + + private static boolean isTerrainBlock(BlockState state) { + return state.isSolid() && !NativeStructureVegetationClearer.isTreeBlock(state); + } + + record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) { + } + + private record SurfaceMaterials(BlockState surface, BlockState subsurface) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java new file mode 100644 index 000000000..f2017ea60 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTerrainIntegrator.java @@ -0,0 +1,508 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.mantle.components.StructureCarveEnvelope; +import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisStructureCarveShape; +import art.arcane.iris.engine.object.IrisStructureTerrain; +import art.arcane.iris.engine.object.IrisStructureTerrainMode; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +public final class NativeStructureTerrainIntegrator { + private static final int AUTO_ENCASE_PADDING = 3; + private static final long CARVE_CEILING_ROLL_SIGNATURE = 0x2A17L; + private static final long CARVE_FLOOR_ROLL_SIGNATURE = 0x5B3DL; + private static final long CARVE_LOBE_SIGNATURE = 0x7C41L; + private static final int MAX_CACHED_CARVE_FOOTPRINTS = 4; + private static final int MAX_CARVE_COLUMNS = 2_000_000; + private static final int MAX_TEMPLATE_OCCUPANCY_CELLS = 4_194_304; + private static final List TEMPLATE_VOID_BLOCKS = List.of(Blocks.AIR, Blocks.STRUCTURE_VOID); + private static final Map CARVE_FOOTPRINTS = + Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75F, true) { + @Override + protected boolean removeEldestEntry( + Map.Entry eldest) { + return size() > MAX_CACHED_CARVE_FOOTPRINTS; + } + }); + + private NativeStructureTerrainIntegrator() { + } + + public static IrisStructureTerrain resolveNativeTerrain(StructureStart start, + IrisStructureTerrain configuredTerrain) { + if (configuredTerrain != null) { + return configuredTerrain; + } + if (start == null || !start.isValid() + || !encasesTerrain(start.getStructure().terrainAdaptation())) { + return null; + } + return new IrisStructureTerrain() + .setMode(IrisStructureTerrainMode.ENCASE) + .setHorizontalPadding(AUTO_ENCASE_PADDING) + .setCeilingPadding(AUTO_ENCASE_PADDING) + .setFloorPadding(AUTO_ENCASE_PADDING); + } + + static boolean encasesTerrain(TerrainAdjustment adjustment) { + return adjustment == TerrainAdjustment.BURY || adjustment == TerrainAdjustment.ENCAPSULATE; + } + + static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain configuredTerrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) { + IrisStructureTerrain terrain = configuredTerrain == null + ? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE) + : configuredTerrain; + IrisStructureTerrainMode mode = terrain.resolvedMode(); + if (mode == IrisStructureTerrainMode.SOURCE || mode == IrisStructureTerrainMode.PRESERVE) { + return; + } + if (mode == IrisStructureTerrainMode.VACUUM) { + carvePieceBoxes(world, area, start, terrain); + return; + } + if (mode == IrisStructureTerrainMode.ENCASE) { + encasePieces(world, area, structureId, start, terrain, paletteBlockResolver); + return; + } + if (mode != IrisStructureTerrainMode.BORE && mode != IrisStructureTerrainMode.FORCE_CARVE) { + throw new IllegalStateException("Native structure terrain mode " + mode + + " is not implemented for '" + structureId + "'"); + } + IrisStructureCarveShape shape = mode == IrisStructureTerrainMode.BORE + ? IrisStructureCarveShape.BOX : terrain.resolvedShape(); + if (shape == IrisStructureCarveShape.BOX) { + carvePieceBoxes(world, area, start, terrain); + return; + } + carveOrganicColumns(world, area, organicCarve( + carveFootprint(start, Math.max(0, terrain.getHorizontalPadding()), + () -> world.getLevel().getStructureManager()), + terrain, shape, carveNoiseIdentity(world, structureId, start))); + } + + static List contentPieceBounds(StructureStart start) { + List bounds = new ArrayList<>(start.getPieces().size()); + for (StructurePiece piece : start.getPieces()) { + if (!NativeStructureReferenceEnvelope.isMarker(piece)) { + bounds.add(piece.getBoundingBox()); + } + } + return List.copyOf(bounds); + } + + /** + * Per-column occupancy of the whole start, cached because a single structure spans many chunks and + * every one of them carves against the same shrinkwrapped footprint. + */ + static StructureCarvingFootprint carveFootprint(StructureStart start, int horizontalPadding, + Supplier templates) { + CarveFootprintKey key = new CarveFootprintKey(start, horizontalPadding); + StructureCarvingFootprint cached = CARVE_FOOTPRINTS.get(key); + if (cached != null) { + return cached; + } + StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns( + sink -> emitCarveColumns(start, templates, sink), horizontalPadding, MAX_CARVE_COLUMNS); + if (footprint == null) { + throw new IllegalStateException("Native structure carve footprint is empty or exceeds " + + MAX_CARVE_COLUMNS + " columns"); + } + CARVE_FOOTPRINTS.put(key, footprint); + return footprint; + } + + static OrganicCarve organicCarve(StructureCarvingFootprint footprint, IrisStructureTerrain terrain, + IrisStructureCarveShape shape, long identity) { + double strength = terrain.resolvedErosionStrength(); + double lobeStrength = terrain.resolvedLobeStrength(); + RNG noiseRng = new RNG(identity); + CNG blob = null; + CNG ceilingRoll = null; + CNG floorRoll = null; + CNG lobe = null; + if (shape == IrisStructureCarveShape.ERODED && strength > 0D) { + blob = CNG.signature(noiseRng); + ceilingRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_CEILING_ROLL_SIGNATURE)); + floorRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_FLOOR_ROLL_SIGNATURE)); + } + if (shape == IrisStructureCarveShape.ERODED && lobeStrength > 0D) { + // A plain single octave channel: the fractured signature noise has no usable low frequency band. + lobe = new CNG(noiseRng.nextParallelRNG(CARVE_LOBE_SIGNATURE), 1D, 1); + } + return new OrganicCarve(footprint, shape, + Math.max(0, terrain.getHorizontalPadding()), + Math.max(0, terrain.getCeilingPadding()), + Math.max(0, terrain.getFloorPadding()), + strength, terrain.resolvedErosionFrequency(), blob, ceilingRoll, floorRoll, + lobe, terrain.resolvedLobeFrequency(), lobeStrength); + } + + static void carveOrganicColumns(WorldGenLevel world, BoundingBox area, OrganicCarve carve) { + StructureCarvingFootprint footprint = carve.footprint(); + int minX = Math.max(area.minX(), footprint.minX()); + int maxX = Math.min(area.maxX(), footprint.maxX()); + int minZ = Math.max(area.minZ(), footprint.minZ()); + int maxZ = Math.min(area.maxZ(), footprint.maxZ()); + if (minX > maxX || minZ > maxZ) { + return; + } + boolean eroded = carve.blob() != null; + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + int index = footprint.indexAt(x, z); + long horizontalDistanceSquared = footprint.distanceSquaredAt(index); + double sideReach = StructureCarveEnvelope.lobedSideReach(carve.lobe(), + carve.lobeFrequency(), carve.lobeStrength(), x, z, carve.horizontalPadding()); + double sideReachSquared = sideReach * sideReach; + if (horizontalDistanceSquared > sideReachSquared) { + continue; + } + double normalizedHorizontal = sideReachSquared == 0D + ? 0D : horizontalDistanceSquared / sideReachSquared; + int sourceMinY = footprint.sourceMinYAt(index); + int sourceMaxY = footprint.sourceMaxYAt(index); + double upReach = eroded + ? StructureCarveEnvelope.lobedUpReach(carve.lobe(), carve.lobeFrequency(), + carve.lobeStrength(), x, z, + StructureCarveEnvelope.erodedUpReach(carve.ceilingRoll(), + carve.frequency(), carve.strength(), x, z, + carve.ceilingPadding())) + : Math.max(1D, carve.ceilingPadding()); + double floorReach = eroded + ? StructureCarveEnvelope.erodedDownReach(carve.floorRoll(), carve.frequency(), + carve.strength(), x, z, carve.floorPadding()) + : carve.floorPadding(); + int columnMinY = Math.max(area.minY(), sourceMinY - (int) Math.floor(floorReach)); + int columnMaxY = Math.min(area.maxY(), + sourceMaxY + (eroded ? (int) Math.ceil(upReach) : carve.ceilingPadding())); + double downReach = Math.max(1D, floorReach); + for (int y = columnMinY; y <= columnMaxY; y++) { + double normalizedVertical = StructureCarveEnvelope.normalizedVerticalDistance( + y, sourceMinY, sourceMaxY, upReach, downReach); + double distanceSquared = normalizedHorizontal + + normalizedVertical * normalizedVertical; + if (distanceSquared > 1D) { + continue; + } + if (distanceSquared > 0D && eroded) { + double noise = carve.blob().fitDouble(0D, 1D, + x * carve.frequency(), y * carve.frequency(), z * carve.frequency()); + if (!StructureCarveEnvelope.shouldCarveOverboreCell( + carve.shape(), distanceSquared, noise, carve.strength())) { + continue; + } + } + world.setBlock(position.set(x, y, z), air, 2); + } + } + } + } + + private static boolean emitCarveColumns(StructureStart start, + Supplier templates, + StructureCarvingFootprint.ColumnSink sink) { + for (StructurePiece piece : start.getPieces()) { + if (NativeStructureReferenceEnvelope.isMarker(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || !emitTemplateColumns(pieceTemplates(poolPiece, templates), + poolPiece.getPosition(), poolPiece.getRotation(), bounds, sink)) { + emitBoxColumns(bounds, sink); + } + } + return true; + } + + /** + * Derives per-column vertical extents from the complement of the template's air and structure-void + * cells, so a carve tracks the actual silhouette instead of the piece's rectangular bounding box. + */ + static boolean emitTemplateColumns(List templates, BlockPos position, + Rotation rotation, BoundingBox bounds, + StructureCarvingFootprint.ColumnSink sink) { + if (templates.isEmpty()) { + return false; + } + int width = bounds.getXSpan(); + int depth = bounds.getZSpan(); + long cells = (long) width * depth * bounds.getYSpan(); + if (cells < 1L || cells > MAX_TEMPLATE_OCCUPANCY_CELLS) { + return false; + } + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(rotation); + boolean[] voidCells = null; + for (StructureTemplate template : templates) { + boolean[] templateVoid = new boolean[(int) cells]; + for (Block ignored : TEMPLATE_VOID_BLOCKS) { + for (StructureTemplate.StructureBlockInfo info + : template.filterBlocks(position, settings, ignored)) { + BlockPos voidPosition = info.pos(); + if (bounds.isInside(voidPosition)) { + templateVoid[templateCellIndex(bounds, width, depth, + voidPosition.getX(), voidPosition.getY(), voidPosition.getZ())] = true; + } + } + } + if (voidCells == null) { + voidCells = templateVoid; + continue; + } + for (int cell = 0; cell < voidCells.length; cell++) { + voidCells[cell] &= templateVoid[cell]; + } + } + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + for (int y = bounds.minY(); y <= bounds.maxY(); y++) { + if (voidCells[templateCellIndex(bounds, width, depth, x, y, z)]) { + continue; + } + if (minY == Integer.MAX_VALUE) { + minY = y; + } + maxY = y; + } + if (minY != Integer.MAX_VALUE) { + sink.column(x, z, minY, maxY); + } + } + } + return true; + } + + private static void emitBoxColumns(BoundingBox bounds, StructureCarvingFootprint.ColumnSink sink) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + sink.column(x, z, bounds.minY(), bounds.maxY()); + } + } + } + + private static int templateCellIndex(BoundingBox bounds, int width, int depth, + int x, int y, int z) { + return ((y - bounds.minY()) * depth + z - bounds.minZ()) * width + x - bounds.minX(); + } + + private static List pieceTemplates(PoolElementStructurePiece poolPiece, + Supplier templates) { + List resolved = new ArrayList<>(1); + collectElementTemplates(poolPiece.getElement(), templates, resolved); + return resolved; + } + + private static void collectElementTemplates(StructurePoolElement element, + Supplier templates, + List resolved) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + collectElementTemplates(child, templates, resolved); + } + return; + } + if (element instanceof SinglePoolElement singleElement) { + resolved.add(NativeStructureReflection.resolveTemplate(singleElement, templates)); + } + } + + private static long carveNoiseIdentity(WorldGenLevel world, String structureId, + StructureStart start) { + return world.getSeed() + ^ ((long) start.getChunkPos().x() * 341873128712L) + ^ ((long) start.getChunkPos().z() * 132897987541L) + ^ (structureId == null ? 0 : structureId.hashCode()); + } + + private static void encasePieces(WorldGenLevel world, BoundingBox area, String structureId, + StructureStart start, IrisStructureTerrain terrain, + NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) { + IrisMaterialPalette palette = terrain.getEncasePalette(); + RNG rng = null; + if (palette != null) { + Objects.requireNonNull(paletteBlockResolver, + "Native structure encase palette requires a platform block resolver"); + rng = new RNG(world.getSeed() ^ (structureId == null ? 0 : structureId.hashCode())); + } + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (BoundingBox bounds : contentPieceBounds(start)) { + BoundingBox shell = paddedPieceArea(area, bounds, terrain); + if (shell == null) { + continue; + } + for (int x = shell.minX(); x <= shell.maxX(); x++) { + for (int z = shell.minZ(); z <= shell.maxZ(); z++) { + for (int y = shell.minY(); y <= shell.maxY(); y++) { + BlockState existing = world.getBlockState(position.set(x, y, z)); + if (!isEncaseable(existing)) { + continue; + } + BlockState fill = palette == null + ? defaultEncaseBlock(y) + : Objects.requireNonNull( + paletteBlockResolver.resolve(palette, rng, x, y, z), + "Encase palette returned no block for " + structureId + " at " + + x + "," + y + "," + z); + world.setBlock(position, fill, 2); + } + } + } + } + } + + static boolean isEncaseable(BlockState state) { + return state.isAir() || !state.getFluidState().isEmpty(); + } + + static BlockState defaultEncaseBlock(int y) { + return y < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState(); + } + + private static void carvePieceBoxes(WorldGenLevel world, BoundingBox area, StructureStart start, + IrisStructureTerrain terrain) { + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (BoundingBox bounds : contentPieceBounds(start)) { + BoundingBox carve = paddedPieceArea(area, bounds, terrain); + if (carve == null) { + continue; + } + for (int x = carve.minX(); x <= carve.maxX(); x++) { + for (int z = carve.minZ(); z <= carve.maxZ(); z++) { + for (int y = carve.minY(); y <= carve.maxY(); y++) { + world.setBlock(position.set(x, y, z), air, 2); + } + } + } + } + } + + private static BoundingBox paddedPieceArea(BoundingBox area, BoundingBox bounds, + IrisStructureTerrain terrain) { + int horizontalPadding = Math.max(0, terrain.getHorizontalPadding()); + int minX = Math.max(area.minX(), bounds.minX() - horizontalPadding); + int minY = Math.max(area.minY(), bounds.minY() - Math.max(0, terrain.getFloorPadding())); + int minZ = Math.max(area.minZ(), bounds.minZ() - horizontalPadding); + int maxX = Math.min(area.maxX(), bounds.maxX() + horizontalPadding); + int maxY = Math.min(area.maxY(), bounds.maxY() + Math.max(0, terrain.getCeilingPadding())); + int maxZ = Math.min(area.maxZ(), bounds.maxZ() + horizontalPadding); + if (minX > maxX || minY > maxY || minZ > maxZ) { + return null; + } + return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + } + + static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area, + StructureStart start, + Supplier templates) { + for (StructurePiece piece : start.getPieces()) { + if (NativeStructureReferenceEnvelope.isMarker(piece)) { + continue; + } + if (!(piece instanceof PoolElementStructurePiece poolPiece) + || poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID + || !intersects(poolPiece.getBoundingBox(), area)) { + continue; + } + int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta(); + StructurePlaceSettings settings = new StructurePlaceSettings() + .setRotation(poolPiece.getRotation()) + .setBoundingBox(area); + clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(), + groundY, settings, templates); + } + } + + private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element, + BlockPos position, int groundY, + StructurePlaceSettings settings, + Supplier templates) { + if (element instanceof ListPoolElement listElement) { + for (StructurePoolElement child : listElement.getElements()) { + clearLegacyTemplateAir(world, child, position, groundY, settings, templates); + } + return; + } + if (!(element instanceof LegacySinglePoolElement legacyElement)) { + return; + } + StructureTemplate template = NativeStructureReflection.resolveTemplate(legacyElement, templates); + clearTemplateAir(world, template, position, groundY, settings); + } + + static void clearTemplateAir(WorldGenLevel world, StructureTemplate template, + BlockPos position, int groundY, + StructurePlaceSettings settings) { + List airBlocks = template.filterBlocks( + position, settings, Blocks.AIR); + BlockState air = Blocks.AIR.defaultBlockState(); + for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) { + BlockPos airPosition = airBlock.pos(); + BlockState existingState = world.getBlockState(airPosition); + if (shouldClearLegacyAir( + airPosition.getY(), groundY, existingState.isAir()) + && !NativeStructureVegetationClearer.isTreeBlock(existingState)) { + world.setBlock(airPosition, air, 2); + } + } + } + + static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) { + return airY >= groundY && !existingAir; + } + + static boolean intersects(BoundingBox first, BoundingBox second) { + return first.maxX() >= second.minX() && first.minX() <= second.maxX() + && first.maxY() >= second.minY() && first.minY() <= second.maxY() + && first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ(); + } + + // StructureStart has no value equality, so the key pins the exact start instance a chunk carves against. + private record CarveFootprintKey(StructureStart start, int padding) { + } + + record OrganicCarve(StructureCarvingFootprint footprint, IrisStructureCarveShape shape, + int horizontalPadding, int ceilingPadding, int floorPadding, + double strength, double frequency, CNG blob, CNG ceilingRoll, CNG floorRoll, + CNG lobe, double lobeFrequency, double lobeStrength) { + } + + public record TerrainTarget(String structureId, StructureStart start, + IrisStructureTerrain terrain) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java new file mode 100644 index 000000000..c5a0b879c --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVegetationClearer.java @@ -0,0 +1,174 @@ +package art.arcane.iris.nativegen; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +public final class NativeStructureVegetationClearer { + private NativeStructureVegetationClearer() { + } + + public static boolean isUndergroundStep(GenerationStep.Decoration step) { + return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES + || step == GenerationStep.Decoration.UNDERGROUND_DECORATION + || step == GenerationStep.Decoration.STRONGHOLDS; + } + + public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step, + boolean configured) { + return configured; + } + + public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area, + List targets) { + if (targets == null || targets.isEmpty()) { + return; + } + VegetationSnapshot snapshot = captureVegetation(chunk, area); + if (snapshot.treeBlockCount() == 0) { + return; + } + boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()]; + for (VegetationTarget target : targets) { + if (target != null && target.force() && target.start() != null && target.start().isValid()) { + markVegetationColumns(area, snapshot, target, clearColumns); + } + } + clearVegetationColumns(world, area, snapshot, clearColumns); + } + + private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) { + int width = area.getXSpan(); + int depth = area.getZSpan(); + BitSet[] columns = new BitSet[width * depth]; + int[] lowestY = new int[columns.length]; + Arrays.fill(lowestY, Integer.MAX_VALUE); + int treeBlockCount = 0; + LevelChunkSection[] sections = chunk.getSections(); + int chunkMinX = chunk.getPos().getMinBlockX(); + int chunkMinZ = chunk.getPos().getMinBlockZ(); + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + LevelChunkSection section = sections[sectionIndex]; + int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; + int minY = Math.max(area.minY(), sectionMinY); + int maxY = Math.min(area.maxY(), sectionMinY + 15); + if (minY > maxY || section.hasOnlyAir() + || !section.maybeHas(NativeStructureVegetationClearer::isTreeBlock)) { + continue; + } + for (int y = minY; y <= maxY; y++) { + int localY = y - sectionMinY; + for (int z = area.minZ(); z <= area.maxZ(); z++) { + int localZ = z - chunkMinZ; + for (int x = area.minX(); x <= area.maxX(); x++) { + int localX = x - chunkMinX; + if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) { + continue; + } + int column = (z - area.minZ()) * width + x - area.minX(); + BitSet treeBlocks = columns[column]; + if (treeBlocks == null) { + treeBlocks = new BitSet(area.getYSpan()); + columns[column] = treeBlocks; + } + treeBlocks.set(y - area.minY()); + lowestY[column] = Math.min(lowestY[column], y); + treeBlockCount++; + } + } + } + } + return new VegetationSnapshot(columns, lowestY, treeBlockCount); + } + + private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot, + VegetationTarget target, boolean[] clearColumns) { + int width = area.getXSpan(); + int[] pieceTops = new int[clearColumns.length]; + Arrays.fill(pieceTops, Integer.MIN_VALUE); + for (StructurePiece piece : target.start().getPieces()) { + if (NativeStructureReferenceEnvelope.isMarker(piece)) { + continue; + } + BoundingBox bounds = piece.getBoundingBox(); + int minX = Math.max(area.minX(), bounds.minX()); + int maxX = Math.min(area.maxX(), bounds.maxX()); + int minZ = Math.max(area.minZ(), bounds.minZ()); + int maxZ = Math.min(area.maxZ(), bounds.maxZ()); + if (minX > maxX || minZ > maxZ) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + for (int x = minX; x <= maxX; x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + pieceTops[column] = Math.max(pieceTops[column], bounds.maxY()); + } + } + } + for (int column = 0; column < pieceTops.length; column++) { + if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) { + continue; + } + if (shouldClearVegetationColumn(pieceTops[column], snapshot.lowestY()[column], target.force())) { + clearColumns[column] = true; + } + } + } + + static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) { + return force || pieceTopY >= lowestTreeY; + } + + private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area, + VegetationSnapshot snapshot, boolean[] clearColumns) { + int width = area.getXSpan(); + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + BlockState air = Blocks.AIR.defaultBlockState(); + for (int z = area.minZ(); z <= area.maxZ(); z++) { + for (int x = area.minX(); x <= area.maxX(); x++) { + int column = (z - area.minZ()) * width + x - area.minX(); + BitSet treeBlocks = snapshot.columns()[column]; + if (!clearColumns[column] || treeBlocks == null) { + continue; + } + for (int bit = treeBlocks.nextSetBit(0); bit >= 0; bit = treeBlocks.nextSetBit(bit + 1)) { + int y = area.minY() + bit; + position.set(x, y, z); + BlockState state = world.getBlockState(position); + if (isTreeBlock(state)) { + world.setBlock(position, air, 2); + } + } + } + } + } + + static boolean isTreeBlock(BlockState state) { + if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) { + return true; + } + String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath(); + return path.endsWith("_log") || path.endsWith("_wood") + || path.endsWith("_stem") || path.endsWith("_hyphae") + || path.endsWith("_leaves"); + } + + public record VegetationTarget(StructureStart start, boolean force) { + } + + private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) { + } +} diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java new file mode 100644 index 000000000..77ca93164 --- /dev/null +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVerticalPlacer.java @@ -0,0 +1,365 @@ +package art.arcane.iris.nativegen; + +import art.arcane.iris.engine.framework.StructureVerticalBounds; +import art.arcane.iris.engine.object.IrisStructureYBand; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.math.RNG; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; +import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction; +import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece; +import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece; +import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.IntBinaryOperator; + +public final class NativeStructureVerticalPlacer { + private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid"; + private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid"; + private static final int MAX_BURIAL_COLUMNS = 2_000_000; + private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24; + private static final String OCEAN_MONUMENT_ID = "minecraft:monument"; + private static final int UNDERGROUND_SURFACE_CLEARANCE = 1; + + private NativeStructureVerticalPlacer() { + } + + public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset, + int seaLevel, int worldMinY, int worldMaxYExclusive, + boolean underground, boolean preserveSourceY, + IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + if (isOceanMonument(structureId)) { + return alignOceanMonumentToSeaLevel( + start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive); + } + if (isAdjustedScatteredStructure(structureId)) { + return alignScatteredStructureToSurface( + start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); + } + return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive, + underground, preserveSourceY, yBand, surfaceHeight); + } + + public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY, + LevelHeightAccessor heightAccessor) { + Objects.requireNonNull(start, "Native structure start must not be null"); + Objects.requireNonNull(source, "Native structure source must not be null"); + Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null"); + if (!start.isValid()) { + return StructureStart.INVALID_START; + } + List pieces = start.getPieces(); + int minY = Integer.MAX_VALUE; + for (StructurePiece piece : pieces) { + minY = Math.min(minY, piece.getBoundingBox().minY()); + } + if (minY == Integer.MAX_VALUE) { + return StructureStart.INVALID_START; + } + int offsetY = Math.subtractExact(targetMinY, minY); + if (offsetY != 0) { + for (StructurePiece piece : pieces) { + moveStructurePiece(piece, offsetY); + } + } + int worldMinY = heightAccessor.getMinY() + 1; + int worldMaxYExclusive = Math.addExact( + heightAccessor.getMinY(), heightAccessor.getHeight()); + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) { + throw new IllegalStateException("Native structure cannot fit target minimum Y " + + targetMinY + " inside world bounds [" + worldMinY + "," + + worldMaxYExclusive + ")"); + } + } + return new StructureStart( + source, + start.getChunkPos(), + start.getReferences(), + new PiecesContainer(List.copyOf(pieces)) + ); + } + + static int alignScatteredStructureToSurface(StructureStart start, String structureId, + int configuredOffset, int worldMinY, + int worldMaxYExclusive, + IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver"); + ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId); + BoundingBox bounds = start.getBoundingBox(); + BoundingBox pieceBounds = piece.getBoundingBox(); + int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight); + int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset); + int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY()); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive); + if (offsetY != 0) { + moveStructureStart(start, bounds, offsetY); + } + setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY())); + return offsetY; + } + + public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY, + int worldMaxYExclusive, boolean underground, + boolean preserveSourceY, IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + BoundingBox bounds = start.getBoundingBox(); + int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY, + worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive); + if (offsetY == 0) { + return 0; + } + moveStructureStart(start, bounds, offsetY); + return offsetY; + } + + private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset, + int worldMinY, int worldMaxYExclusive, boolean underground, + boolean preserveSourceY, IrisStructureYBand yBand, + IntBinaryOperator surfaceHeight) { + if (preserveSourceY) { + return requestedOffset; + } + if (yBand != null) { + return resolveYBandOffset(bounds, yBand, start.getChunkPos()); + } + if (underground) { + return resolveBuriedOffset( + bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight); + } + return requestedOffset; + } + + static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel, + int worldMinY, int worldMaxYExclusive) { + OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start); + BoundingBox bounds = start.getBoundingBox(); + int targetMinY = Math.addExact( + Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset); + int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY()); + int offsetY = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive); + if (offsetY != requestedOffset) { + throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel + + " with configured offset " + configuredOffset + " inside world bounds [" + + worldMinY + "," + worldMaxYExclusive + ")"); + } + if (offsetY == 0) { + return 0; + } + moveStructureStart(start, bounds, offsetY); + return offsetY; + } + + static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId, + int configuredOffset, int seaLevel, + int worldMinY, int worldMaxYExclusive) { + if (isOceanMonument(structureId)) { + alignOceanMonumentToSeaLevel( + start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive); + } + } + + private static boolean isOceanMonument(String structureId) { + return OCEAN_MONUMENT_ID.equals(structureId); + } + + private static boolean isAdjustedScatteredStructure(String structureId) { + return DESERT_PYRAMID_ID.equals(structureId) || JUNGLE_PYRAMID_ID.equals(structureId); + } + + private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start, + String structureId) { + Objects.requireNonNull(start, "Scattered native structure start must not be null"); + List pieces = start.getPieces(); + if (pieces.size() != 1) { + throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found " + + pieces.size()); + } + StructurePiece piece = pieces.get(0); + if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) { + return desertPyramid; + } + if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) { + return jungleTemple; + } + throw new IllegalStateException(structureId + " contains unexpected piece " + + piece.getClass().getName()); + } + + private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds, + IntBinaryOperator surfaceHeight) { + if (DESERT_PYRAMID_ID.equals(structureId)) { + return lowestSurfaceY(bounds, surfaceHeight); + } + if (JUNGLE_PYRAMID_ID.equals(structureId)) { + return averageSurfaceY(bounds, surfaceHeight); + } + throw new IllegalStateException("Unsupported scattered native structure " + structureId); + } + + private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { + int lowestY = Integer.MAX_VALUE; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z)); + } + } + if (lowestY == Integer.MAX_VALUE) { + throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); + } + return lowestY; + } + + private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) { + long totalY = 0L; + long columns = 0L; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + totalY += surfaceHeight.applyAsInt(x, z); + columns++; + } + } + if (columns == 0L) { + throw new IllegalStateException("Scattered native structure has an empty terrain footprint"); + } + return Math.toIntExact(totalY / columns); + } + + private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) { + try { + NativeStructureReflection.ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot lock scattered native structure height", error); + } + } + + private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) { + Objects.requireNonNull(start, "Ocean monument start must not be null"); + List pieces = start.getPieces(); + if (pieces.size() != 1 || !(pieces.get(0) instanceof OceanMonumentPieces.MonumentBuilding building)) { + throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found " + + pieces.size() + " top-level pieces"); + } + return building; + } + + private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) { + for (StructurePiece piece : start.getPieces()) { + moveStructurePiece(piece, offsetY); + } + cachedBounds.move(0, offsetY, 0); + } + + private static void moveStructurePiece(StructurePiece piece, int offsetY) { + piece.move(0, offsetY, 0); + if (piece instanceof OceanMonumentPieces.MonumentBuilding building) { + for (StructurePiece child : monumentChildPieces(building)) { + child.move(0, offsetY, 0); + } + } + if (piece instanceof PoolElementStructurePiece poolPiece) { + List junctions = poolPiece.getJunctions(); + for (int i = 0; i < junctions.size(); i++) { + JigsawJunction junction = junctions.get(i); + junctions.set(i, new JigsawJunction( + junction.getSourceX(), + junction.getSourceGroundY() + offsetY, + junction.getSourceZ(), + junction.getDeltaY(), + junction.getDestProjection())); + } + } + } + + static List monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) { + Object value; + try { + value = NativeStructureReflection.MonumentChildPiecesAccess.FIELD.get(building); + } catch (IllegalAccessException error) { + throw new IllegalStateException("Cannot read Ocean Monument child pieces", error); + } + if (!(value instanceof List children)) { + throw new IllegalStateException("Ocean Monument child-pieces field is not a list"); + } + List pieces = new ArrayList<>(children.size()); + for (Object child : children) { + if (!(child instanceof StructurePiece monumentPiece) + || child.getClass().getEnclosingClass() != OceanMonumentPieces.class) { + throw new IllegalStateException("Ocean Monument child-pieces list contains " + + (child == null ? "null" : child.getClass().getName())); + } + pieces.add(monumentPiece); + } + return List.copyOf(pieces); + } + + static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) { + Objects.requireNonNull(bounds, "Native structure bounds must not be null"); + Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk"); + int target = yBandTargetMidpointY( + bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk); + return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY())); + } + + static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) { + int height = Math.subtractExact(maxY, minY); + int lowest = Math.addExact(bandMin, height / 2); + int highest = Math.subtractExact(bandMax, height - height / 2); + if (lowest > highest) { + // The band is shorter than the structure, so only its midpoint can be honoured. + return Math.floorDiv(Math.addExact(bandMin, bandMax), 2); + } + int span = highest - lowest + 1; + if (span == 1) { + return lowest; + } + long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L; + return lowest + new RNG(identity).nextInt(span); + } + + private static int midpointY(int minY, int maxY) { + return minY + (maxY - minY) / 2; + } + + static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY, + int worldMaxYExclusive, IntBinaryOperator surfaceHeight) { + Objects.requireNonNull(bounds, "Native structure bounds must not be null"); + Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver"); + long width = (long) bounds.maxX() - bounds.minX() + 1L; + long depth = (long) bounds.maxZ() - bounds.minZ() + 1L; + if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) { + throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds " + + MAX_BURIAL_COLUMNS + " columns"); + } + int maximumOffset = requestedOffset; + for (int x = bounds.minX(); x <= bounds.maxX(); x++) { + for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) { + int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE; + maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY()); + } + } + int clampedOffset = StructureVerticalBounds.clampOffset( + bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive); + if (clampedOffset > maximumOffset) { + IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ() + + " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset); + } + return clampedOffset; + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmaps.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java similarity index 90% rename from adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmaps.java rename to adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java index b01ed70dd..66f7bf1be 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/WorldgenTerrainHeightmaps.java +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/WorldgenTerrainHeightmaps.java @@ -1,4 +1,4 @@ -package art.arcane.iris.core.nms.v26_2_R1; +package art.arcane.iris.nativegen; import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; @@ -29,15 +29,15 @@ import java.util.function.IntBinaryOperator; * Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG * does not. */ -final class WorldgenTerrainHeightmaps { +public final class WorldgenTerrainHeightmaps { private static final int COLUMNS = 256; private static final int PLACEMENT_CHUNK_MARGIN = 1; private WorldgenTerrainHeightmaps() { } - static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY, - IntBinaryOperator floorFirstFreeY) { + public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk"); Objects.requireNonNull(surfaceFirstFreeY, "Iris worldgen heightmap priming requires a surface height resolver"); @@ -47,9 +47,9 @@ final class WorldgenTerrainHeightmaps { write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY); } - static void primeStructurePlacement(WorldGenLevel world, List starts, - IntBinaryOperator surfaceFirstFreeY, - IntBinaryOperator floorFirstFreeY) { + public static void primeStructurePlacement(WorldGenLevel world, List starts, + IntBinaryOperator surfaceFirstFreeY, + IntBinaryOperator floorFirstFreeY) { Objects.requireNonNull(world, "Iris worldgen heightmap priming requires a generation level"); if (starts == null || starts.isEmpty()) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java index 134954a7a..80b796695 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java @@ -49,8 +49,10 @@ import java.util.stream.Stream; final class IrisModdedBiomeSource extends BiomeSource { private static final int BIOME_CACHE_MAX = 262144; + private static final int UNRESOLVED_WARN_KEYS_MAX = 256; private final BiomeSource serializedSource; + private final Set warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet(); private final ConcurrentHashMap> visibleBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap> structureBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap> surfaceStructureBiomeCache = new ConcurrentHashMap<>(); @@ -70,6 +72,7 @@ final class IrisModdedBiomeSource extends BiomeSource { visibleBiomeCache.clear(); structureBiomeCache.clear(); surfaceStructureBiomeCache.clear(); + warnedUnresolvedBiomeKeys.clear(); possibleStructureBiomeKeys = null; for (StructureStateBiomeSource source : structureStateSources) { source.clearCache(); @@ -332,7 +335,8 @@ final class IrisModdedBiomeSource extends BiomeSource { IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome( resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); if (customBiome == null) { - return fallbackBiome(registry, quartX, quartY, quartZ, sampler); + return fallbackBiome(registry, "custom derivative of '" + + resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler); } biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId()); } else if (resolution.underground()) { @@ -344,7 +348,7 @@ final class IrisModdedBiomeSource extends BiomeSource { } Holder resolved = resolveHolder(registry, biomeKey); return resolved == null - ? fallbackBiome(registry, quartX, quartY, quartZ, sampler) + ? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler) : resolved; } @@ -567,12 +571,29 @@ final class IrisModdedBiomeSource extends BiomeSource { return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT); } - private Holder fallbackBiome(Registry registry, int quartX, int quartY, int quartZ, + private Holder fallbackBiome(Registry registry, String unresolvedKey, + int quartX, int quartY, int quartZ, Climate.Sampler sampler) { Holder plains = resolveHolder(registry, "minecraft:plains"); + warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains", + quartX, quartY, quartZ); return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains; } + private void warnUnresolvedBiome(String unresolvedKey, String fallback, + int quartX, int quartY, int quartZ) { + String key = unresolvedKey == null || unresolvedKey.isBlank() ? "" : unresolvedKey; + if (!warnedUnresolvedBiomeKeys.add(key)) { + return; + } + if (warnedUnresolvedBiomeKeys.size() > UNRESOLVED_WARN_KEYS_MAX) { + warnedUnresolvedBiomeKeys.clear(); + } + ModdedIrisLog.warn("Iris biome " + key + " is not registered; using " + fallback + + " at quart " + quartX + "," + quartY + "," + quartZ + + " (wrong biome generates; regenerate the forced datapack and restart)"); + } + private static long packNoiseKey(int x, int y, int z) { return (((long) x & 67108863L) << 38) | (((long) z & 67108863L) << 12) 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 e458081de..38b827237 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 @@ -23,27 +23,15 @@ import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionLease; -import art.arcane.iris.engine.framework.IrisStructureLocator; -import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; import art.arcane.iris.engine.framework.NativeStructureStartPlan; -import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisMaterialPalette; -import art.arcane.iris.engine.object.IrisNativeStructureDecision; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.nativegen.NativeStructureGenerationException; import art.arcane.iris.nativegen.NativeStructureStartInjector; -import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope; import art.arcane.iris.nativegen.NativeStructureLocateResults; -import art.arcane.iris.nativegen.NativeStructurePostProcessor; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.hunk.Hunk; -import art.arcane.volmlib.util.math.RNG; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; @@ -54,9 +42,7 @@ import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.RegistryAccess; -import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; -import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; @@ -83,15 +69,12 @@ import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.chunk.LevelChunkSection; -import net.minecraft.world.level.levelgen.GenerationStep; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.RandomState; import net.minecraft.world.level.levelgen.RandomSupport; import net.minecraft.world.level.levelgen.WorldgenRandom; -import net.minecraft.world.level.levelgen.XoroshiroRandomSource; import net.minecraft.world.level.levelgen.blending.Blender; -import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -100,98 +83,41 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntBinaryOperator; public final class IrisModdedChunkGenerator extends ChunkGenerator { - private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096; - private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance instance) -> instance.group( BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource), Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey) ).apply(instance, IrisModdedChunkGenerator::new)); - private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger(); - private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem(); - private static volatile ExecutorService genPool = createGenPool(); - public static void startGenPool() { - ExecutorService pool = genPool; - if (pool == null || pool.isShutdown()) { - genPool = createGenPool(); - } + ModdedGenPool.start(); } public static void shutdownGenPool() { - ExecutorService pool = genPool; - if (pool != null) { - pool.shutdownNow(); - } - } - - private static boolean detectParallelChunkSystem() { - String[] markers = { - "com.ishland.c2me.base.ModProperties", - "com.ishland.c2me.base.common.config.C2MEConfig", - "com.ishland.c2me.opts.chunkio.ModProperties", - "ca.spottedleaf.moonrise.common.util.MoonriseCommon" - }; - for (String marker : markers) { - try { - Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader()); - return true; - } catch (Throwable ignored) { - } - } - return false; - } - - private static ExecutorService createGenPool() { - int threads = Math.max(2, Runtime.getRuntime().availableProcessors()); - ThreadPoolExecutor pool = new ThreadPoolExecutor( - threads, threads, 30L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), - runnable -> { - Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet()); - thread.setDaemon(true); - return thread; - }); - pool.allowCoreThreadTimeOut(true); - return pool; + ModdedGenPool.shutdown(); } private final String dimensionKey; private final String defaultPack; private final String defaultDimensionKey; private final BiomeSource serializedBiomeSource; - private final IrisModdedBiomeSource structureBiomeSource; - private final EngineBinding engineBinding = new EngineBinding<>(60L, TimeUnit.SECONDS); - private final ConcurrentHashMap> vanillaSpawnBiomes = new ConcurrentHashMap<>(); - private final ConcurrentHashMap> mergedSpawnTables = new ConcurrentHashMap<>(); - private final ConcurrentHashMap worldCheckStructureShifts = new ConcurrentHashMap<>(); + final IrisModdedBiomeSource structureBiomeSource; + private final ModdedEngineBinding engineBinding = new ModdedEngineBinding<>(60L, TimeUnit.SECONDS); + private final ModdedNativeStructureStage nativeStructures = new ModdedNativeStructureStage(this); + private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this); private final AtomicBoolean announced = new AtomicBoolean(false); - private volatile boolean vanillaSpawnBiomesInitialized; private volatile boolean unloading; private volatile Engine engine; private volatile String activePack; @@ -199,8 +125,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { private volatile long seedOverride = Long.MIN_VALUE; private volatile long lastChunkGenAt = 0L; private volatile Set configuredStructureBiomeKeys; - private volatile ConfiguredPack configuredPack; - private volatile StructureStepCache structureStepCache; + private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack; public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) { this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource)); @@ -262,8 +187,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { this.engineBinding.complete(replacement); this.announced.set(false); this.structureBiomeSource.clearCaches(); - this.worldCheckStructureShifts.clear(); - resetVanillaSpawnBiomes(); + this.nativeStructures.clearWorldCheckStructureShifts(); + this.spawnTables.resetVanillaSpawnBiomes(); } public synchronized void unbindEngine() { @@ -287,8 +212,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { this.engineBinding.reset(); this.announced.set(false); this.structureBiomeSource.clearCaches(); - this.worldCheckStructureShifts.clear(); - resetVanillaSpawnBiomes(); + this.nativeStructures.clearWorldCheckStructureShifts(); + this.spawnTables.resetVanillaSpawnBiomes(); } private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) { @@ -301,8 +226,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { this.engineBinding.reset(); this.announced.set(false); this.structureBiomeSource.clearCaches(); - this.worldCheckStructureShifts.clear(); - resetVanillaSpawnBiomes(); + this.nativeStructures.clearWorldCheckStructureShifts(); + this.spawnTables.resetVanillaSpawnBiomes(); } public synchronized void resetToDefault() { @@ -335,9 +260,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { Engine current = engine(); try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate"); IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { - Pair> irisPlaced = findNearestIrisStructure( + Pair> irisPlaced = nativeStructures.findNearestIrisStructure( level, holders, pos, Math.max(1, radius), findUnexplored, current); - HolderSet reachable = filterReachableNativeStructures(level, holders, current); + HolderSet reachable = nativeStructures.filterReachableNativeStructures( + level, holders, current); Pair> nativeLocated = reachable.size() == 0 ? null : super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored); @@ -349,67 +275,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { return structure != null && structureBiomeSource.isStructureReachable(structure); } - private Pair> findNearestIrisStructure(ServerLevel level, - HolderSet holders, - BlockPos pos, int radius, boolean findUnexplored, - Engine current) { - if (findUnexplored) { - return null; - } - Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); - BlockPos best = null; - Holder bestHolder = null; - long bestDistance = Long.MAX_VALUE; - for (Holder holder : holders) { - Identifier id = registry.getKey(holder.value()); - if (id == null) { - throw new IllegalStateException("Native structure locate received an unregistered structure holder"); - } - String structureId = id.toString(); - if (!IrisStructureLocator.isPlaced(current, structureId)) { - continue; - } - IrisStructureLocator.LocateResult result = IrisStructureLocator.locate( - current, structureId, pos.getX(), pos.getZ(), radius); - if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { - throw new IllegalStateException("Iris structure locate reached its safety limit for " - + structureId + " within " + radius + " chunks"); - } - if (!result.found()) { - continue; - } - long dx = (long) result.originX() - pos.getX(); - long dz = (long) result.originZ() - pos.getZ(); - long distance = dx * dx + dz * dz; - if (distance < bestDistance) { - bestDistance = distance; - best = new BlockPos(result.originX(), result.baseY(), result.originZ()); - bestHolder = holder; - } - } - return best == null ? null : Pair.of(best, bestHolder); - } - - private HolderSet filterReachableNativeStructures(ServerLevel level, HolderSet holders, - Engine current) { - Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); - List> kept = new ArrayList<>(holders.size()); - for (Holder holder : holders) { - Identifier id = registry.getKey(holder.value()); - if (id == null) { - throw new IllegalStateException("Native structure filtering received an unregistered structure holder"); - } - String key = id.toString(); - IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current, - key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step())); - if (!decision.generate() || !structureBiomeSource.isStructureReachable(holder)) { - continue; - } - kept.add(holder); - } - return kept.size() == holders.size() ? holders : HolderSet.direct(kept); - } - private ServerLevel boundLevel() { MinecraftServer server = ModdedEngineBootstrap.currentServer(); if (server == null) { @@ -423,7 +288,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { return null; } - private Engine engine() { + Engine engine() { requireBindingAllowed(); Engine cached = engine; requireCompletedShutdown(cached); @@ -516,7 +381,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { + "and deny individual structures through importedStructures.disabled"); } - private Engine engineOrNull() { + Engine engineOrNull() { requireBindingAllowed(); Engine cached = engine; requireCompletedShutdown(cached); @@ -607,7 +472,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { if (current != null && !current.isClosed() && !current.isClosing()) { try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys"); IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { - Set resolved = collectConfiguredBiomeKeys( + Set resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys( current.getAllBiomes(), current.getDimension().getLoadKey()); configuredStructureBiomeKeys = resolved; return resolved; @@ -617,15 +482,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { } } } - ConfiguredPack configured = configuredPack(); - Set resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data()); + ModdedDimensionMetadata.ConfiguredPack configured = configuredPack(); + Set resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys( + configured.dimension(), configured.data()); configuredStructureBiomeKeys = resolved; return resolved; } } - private ConfiguredPack configuredPack() { - ConfiguredPack cached = configuredPack; + private ModdedDimensionMetadata.ConfiguredPack configuredPack() { + ModdedDimensionMetadata.ConfiguredPack cached = configuredPack; if (cached != null) { return cached; } @@ -641,60 +507,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { throw new IllegalStateException("Iris dimension '" + activeDimensionKey + "' missing from pack " + packDirectory.getAbsolutePath()); } - ConfiguredPack resolved = new ConfiguredPack(data, dimension, dimensionMetadata(dimension)); + ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack( + data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension)); configuredPack = resolved; return resolved; } } - static DimensionMetadata dimensionMetadata(IrisDimension dimension) { - int minY = dimension.getMinHeight(); - int maxY = dimension.getMaxHeight(); - if (maxY <= minY) { - throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey() - + "' has invalid height range " + minY + ".." + maxY); - } - return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight()); - } - - static Set collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) { - LinkedHashSet keys = new LinkedHashSet<>( - collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey())); - for (IrisRegion region : dimension.getAllRegions(() -> data)) { - if (region == null) { - continue; - } - if (!region.getSeaBiomes().isEmpty()) { - keys.add("minecraft:the_void"); - } - if (!region.getShoreBiomes().isEmpty()) { - keys.add("minecraft:beach"); - } - } - return Set.copyOf(keys); - } - - static Set collectConfiguredBiomeKeys(Iterable biomes, String dimensionLoadKey) { - LinkedHashSet keys = new LinkedHashSet<>(); - String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT); - for (IrisBiome irisBiome : biomes) { - if (irisBiome == null) { - continue; - } - Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey()); - if (derivative != null) { - keys.add(derivative.toString().toLowerCase(Locale.ROOT)); - } - if (!irisBiome.isCustom()) { - continue; - } - for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { - keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT)); - } - } - return Set.copyOf(keys); - } - public String dimensionKey() { return dimensionKey; } @@ -715,8 +534,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { public void onHotload() { configuredStructureBiomeKeys = null; structureBiomeSource.clearCaches(); - worldCheckStructureShifts.clear(); - resetVanillaSpawnBiomes(); + nativeStructures.clearWorldCheckStructureShifts(); + spawnTables.resetVanillaSpawnBiomes(); } @Override @@ -730,8 +549,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { } Registry registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME); - initializeVanillaSpawnBiomes(registry); - Holder vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value()); + spawnTables.initializeVanillaSpawnBiomes(registry); + Holder vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value()); if (vanillaSpawnBiome == null) { return explicitSpawns; } @@ -744,57 +563,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { return explicitSpawns; } - SpawnTableKey key = new SpawnTableKey(biome.value(), category); - return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)); - } - - private synchronized void initializeVanillaSpawnBiomes(Registry registry) { - if (vanillaSpawnBiomesInitialized) { - return; - } - Engine current = engineOrNull(); - if (current == null) { - return; - } - - try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes"); - IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { - String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT); - for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) { - if (irisBiome == null || !irisBiome.isCustom()) { - continue; - } - Holder vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey()); - if (vanillaHolder == null) { - continue; - } - for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { - Holder customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId()); - if (customHolder != null) { - vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); - } - } - } - vanillaSpawnBiomesInitialized = true; - } - } - - private Holder resolveBiomeHolder(Registry registry, String key) { - if (key == null || key.isBlank()) { - return null; - } - Identifier identifier = Identifier.tryParse(key); - if (identifier == null) { - return null; - } - Optional> reference = registry.get(identifier); - return reference.>map((Holder.Reference value) -> value).orElse(null); - } - - private synchronized void resetVanillaSpawnBiomes() { - vanillaSpawnBiomes.clear(); - mergedSpawnTables.clear(); - vanillaSpawnBiomesInitialized = false; + return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns); } @Override @@ -817,13 +586,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { PlatformBlockState air = IrisPlatforms.get().registries().air(); - if (PARALLEL_CHUNK_SYSTEM) { + if (ModdedGenPool.parallelChunkSystem()) { return CompletableFuture.completedFuture( generateTerrain(chunk, generationEngine, pos, air)); } return CompletableFuture.supplyAsync( () -> generateTerrain(chunk, generationEngine, pos, air), - genPool); + ModdedGenPool.pool()); } private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, @@ -944,7 +713,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { Engine current = engine(); try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration"); IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { - placeVanillaStructures(level, chunk, structureManager); + nativeStructures.placeVanillaStructures(level, chunk, structureManager); } } @@ -967,7 +736,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { this, structureBiomeSource )); - adjustGeneratedStructures( + nativeStructures.adjustGeneratedStructures( registryAccess, chunk, previousStarts, configuredStarts, current, templateManager); } } @@ -981,269 +750,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { } } - private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk, - Map previousStarts, - Map configuredStarts, - Engine current, - StructureTemplateManager templateManager) { - Registry registry = registryAccess.lookupOrThrow(Registries.STRUCTURE); - ChunkPos chunkPos = chunk.getPos(); - for (Map.Entry entry : chunk.getAllStarts().entrySet()) { - Structure structure = entry.getKey(); - StructureStart start = entry.getValue(); - if (!start.isValid() || previousStarts.get(structure) == start) { - continue; - } - if (configuredStarts.containsKey(structure)) { - recordWorldCheckStructureShift( - configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0); - continue; - } - Identifier id = registry.getKey(structure); - String structureId = id == null ? null : id.toString(); - if (structureId == null) { - throw NativeStructureGenerationException.failure( - "resolution", null, chunkPos.x(), chunkPos.z()); - } - boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step()); - IrisNativeStructureDecision decision; - try { - decision = NativeStructureGenerationPolicy.resolve(current, - structureId, undergroundStep); - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "policy resolution", structureId, chunkPos.x(), chunkPos.z(), error); - } - if (!decision.generate()) { - chunk.setStartForStructure(structure, StructureStart.INVALID_START); - continue; - } - int offsetY; - try { - offsetY = NativeStructurePostProcessor.applyVerticalPlacement( - start, - structureId, - decision.yShift(), - getSeaLevel(), - chunk.getMinY(), - chunk.getMinY() + chunk.getHeight(), - undergroundStep, - decision.preserveSourceY(), - decision.yBand(), - (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); - StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( - start, structure, start.getReferences(), templateManager, - NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain())); - chunk.setStartForStructure(structure, wrapped); - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error); - } - recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY); - } - } - - private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { - if (!structureManager.shouldGenerateStructures()) { - ChunkPos disabledChunk = chunk.getPos(); - throw new IllegalStateException("Iris cannot generate native structures in chunk " - + disabledChunk.x() + "," + disabledChunk.z() - + " because generate-structures=false disables them outside the pack; set generate-structures=true, " - + "restart the server, and deny individual structures through importedStructures.disabled"); - } - ChunkPos chunkPos = chunk.getPos(); - SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); - BlockPos origin = sectionPos.origin(); - Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); - List> byStep = structuresByStep(registry); - WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); - long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); - BoundingBox area = writableArea(chunk); - int steps = GenerationStep.Decoration.values().length; - Engine current = engine(); - List placementGroups = new ArrayList<>(); - List nativeStarts = new ArrayList<>(); - List vegetationTargets = new ArrayList<>(); - List terrainTargets = new ArrayList<>(); - for (int step = 0; step < steps; step++) { - int index = 0; - for (Structure structure : byStep.get(step)) { - Identifier id = registry.getKey(structure); - String structureId = id == null ? null : id.toString(); - if (structureId == null) { - throw NativeStructureGenerationException.failure( - "resolution", null, chunkPos.x(), chunkPos.z()); - } - try { - IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current, - structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step())); - List starts = structureManager.startsForStructure(sectionPos, structure); - List resolvedPlacements = new ArrayList<>(starts.size()); - for (StructureStart start : starts) { - NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan( - current, structureId, start.getChunkPos().x(), start.getChunkPos().z()); - IrisNativeStructureDecision decision = plan == null - ? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan); - if (!decision.generate()) { - continue; - } - resolvedPlacements.add(new NativePlacement(start, decision)); - terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget( - structureId, start, - NativeStructurePostProcessor.resolveNativeTerrain( - start, decision.terrain()))); - if (plan == null || !plan.placement().isUnderground()) { - nativeStarts.add(start); - } - boolean clearEntireFootprint = NativeStructurePostProcessor - .shouldClearEntireVegetationFootprint( - structure.step(), decision.clearVegetation()); - vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget( - start, clearEntireFootprint)); - } - if (!resolvedPlacements.isEmpty()) { - placementGroups.add(new NativePlacementGroup( - structureId, index, step, List.copyOf(resolvedPlacements))); - } - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "resolution", structureId, chunkPos.x(), chunkPos.z(), error); - } - index++; - } - } - try { - NativeStructurePostProcessor.prepareSurfaceStructures( - world, area, nativeStarts, - (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "terrain integration", nativeStructureBatchContext(placementGroups), - chunkPos.x(), chunkPos.z(), error); - } - try { - NativeStructurePostProcessor.clearIntersectingVegetation( - world, chunk, area, vegetationTargets); - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "vegetation cleanup", nativeStructureBatchContext(placementGroups), - chunkPos.x(), chunkPos.z(), error); - } - try { - NativeStructurePostProcessor.prepareTerrain( - world, area, terrainTargets, this::resolvePaletteBlock); - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "terrain carving", nativeStructureBatchContext(placementGroups), - chunkPos.x(), chunkPos.z(), error); - } - for (NativePlacementGroup group : placementGroups) { - random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step()); - try { - for (NativePlacement placement : group.placements()) { - placeVanillaStructure(world, structureManager, random, area, chunkPos, - group.structureId(), placement.start(), placement.decision()); - } - } catch (Throwable error) { - throw NativeStructureGenerationException.failure( - "placement", group.structureId(), chunkPos.x(), chunkPos.z(), error); - } - } - } - - private static String nativeStructureBatchContext(List placementGroups) { - if (placementGroups.isEmpty()) { - return ""; - } - StringBuilder context = new StringBuilder("["); - for (int i = 0; i < placementGroups.size(); i++) { - if (i > 0) { - context.append(", "); - } - context.append(placementGroups.get(i).structureId()); - } - return context.append(']').toString(); - } - - private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, - WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, - String structureId, StructureStart start, - IrisNativeStructureDecision decision) { - NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos, - structureId, start, decision, this::resolvePaletteBlock, - (x, z) -> engine().getHeight(x, z, true) + engine().getMinHeight()); - } - - private List> structuresByStep(Registry registry) { - StructureStepCache cached = structureStepCache; - if (cached != null && cached.registry() == registry) { - return cached.structures(); - } - synchronized (this) { - cached = structureStepCache; - if (cached != null && cached.registry() == registry) { - return cached.structures(); - } - int steps = GenerationStep.Decoration.values().length; - List> grouped = new ArrayList<>(steps); - for (int step = 0; step < steps; step++) { - grouped.add(new ArrayList<>()); - } - for (Structure structure : registry) { - grouped.get(structure.step().ordinal()).add(structure); - } - for (int step = 0; step < steps; step++) { - grouped.set(step, List.copyOf(grouped.get(step))); - } - List> resolved = List.copyOf(grouped); - structureStepCache = new StructureStepCache(registry, resolved); - return resolved; - } - } - - private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) { - if (!WORLD_CHECK_ENABLED || structureId == null) { - return; - } - if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) { - worldCheckStructureShifts.clear(); - } - worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY); - } - Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) { - if (structureId == null || startChunk == null) { - return null; - } - return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack())); - } - - private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng, - int x, int y, int z) { - PlatformBlockState platformState = palette.get(rng, x, y, z, engine().getData()); - if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) { - throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at " - + x + "," + y + "," + z); - } - return blockState; - } - - private BoundingBox writableArea(ChunkAccess chunk) { - ChunkPos chunkPos = chunk.getPos(); - int minX = chunkPos.getMinBlockX(); - int minZ = chunkPos.getMinBlockZ(); - int minY = chunk.getMinY(); - int maxY = minY + chunk.getHeight() - 1; - return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15); + return nativeStructures.worldCheckStructureShift(structureId, startChunk); } @Override public void spawnOriginalMobs(WorldGenRegion region) { Registry registry = region.registryAccess().lookupOrThrow(Registries.BIOME); - initializeVanillaSpawnBiomes(registry); + spawnTables.initializeVanillaSpawnBiomes(registry); ChunkPos center = region.getCenter(); Holder visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); - Holder vanillaBiome = vanillaSpawnBiomes.get(visibleBiome.value()); + Holder vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value()); WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed())); random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ()); NaturalSpawner.spawnMobsForChunkGeneration( @@ -1276,13 +793,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { @Override public int getSpawnHeight(LevelHeightAccessor heightAccessor) { - return clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight()); - } - - static int clampSpawnHeight(int minY, int height) { - int minimum = minY + 1; - int maximum = minY + height - 2; - return Math.max(minimum, Math.min(maximum, 96)); + return ModdedDimensionMetadata.clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight()); } @Override @@ -1320,7 +831,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { } } - private GenerationSessionLease requireGenerationLease(Engine current, String operation) { + GenerationSessionLease requireGenerationLease(Engine current, String operation) { try { return current.acquireGenerationLease(operation); } catch (GenerationSessionException exception) { @@ -1333,80 +844,4 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { info.add("Iris dimension: " + dimensionKey); } - private record SpawnTableKey(Biome biome, MobCategory category) { - } - - private record StructureStepCache(Registry registry, List> structures) { - } - - private record NativeStructureStartKey(String structureId, long chunkPosition) { - } - - private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) { - } - - private record NativePlacementGroup(String structureId, int featureIndex, int step, - List placements) { - } - - record DimensionMetadata(int minY, int maxY, int seaLevel) { - int depth() { - return maxY - minY; - } - } - - private record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) { - } - - static final class EngineBinding { - private final long timeout; - private final TimeUnit timeoutUnit; - private volatile CompletableFuture future = new CompletableFuture<>(); - - EngineBinding(long timeout, TimeUnit timeoutUnit) { - this.timeout = timeout; - this.timeoutUnit = timeoutUnit; - } - - T await(String dimensionKey) { - try { - return future.get(timeout, timeoutUnit); - } catch (InterruptedException error) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting for Iris generator '" - + dimensionKey + "' to bind", error); - } catch (ExecutionException error) { - throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", - error.getCause()); - } catch (TimeoutException error) { - throw new IllegalStateException("Timed out waiting for Iris generator '" - + dimensionKey + "' to bind", error); - } - } - - void complete(T value) { - future.complete(value); - } - - void fail(Throwable error) { - future.completeExceptionally(error); - } - - void throwIfFailed(String dimensionKey) { - CompletableFuture current = future; - if (!current.isCompletedExceptionally()) { - return; - } - try { - current.join(); - } catch (CompletionException error) { - throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", - error.getCause()); - } - } - - void reset() { - future = new CompletableFuture<>(); - } - } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java index 27d851544..f455285c0 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockResolution.java @@ -45,6 +45,7 @@ import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; public final class ModdedBlockResolution { @@ -86,6 +87,8 @@ public final class ModdedBlockResolution { "acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves"); private static final BlockState AIR = Blocks.AIR.defaultBlockState(); private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L); + private static final int REPORTED_FAILURE_KEYS_MAX = 256; + private static final Set REPORTED_FAILURE_KEYS = ConcurrentHashMap.newKeySet(); private ModdedBlockResolution() { } @@ -152,6 +155,17 @@ public final class ModdedBlockResolution { return map; } + private static void reportResolveFailure(String key, Throwable error) { + String failureKey = key == null ? "" : key; + if (!REPORTED_FAILURE_KEYS.add(failureKey)) { + return; + } + if (REPORTED_FAILURE_KEYS.size() > REPORTED_FAILURE_KEYS_MAX) { + REPORTED_FAILURE_KEYS.clear(); + } + IrisLogging.reportError("Iris block data '" + failureKey + "' failed to resolve", error); + } + private static void warnUnresolved(String key, String message) { if (UNRESOLVED.firstOccurrence(key)) { IrisLogging.warn(message); @@ -239,7 +253,7 @@ public final class ModdedBlockResolution { return bdx; } catch (Throwable e) { - e.printStackTrace(); + reportResolveFailure(bdxf, e); if (warn) { warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'"); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java index 0cf22b69d..f4be1800f 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBlockState.java @@ -38,6 +38,7 @@ public final class ModdedBlockState implements PlatformBlockState { private final String key; private final String namespace; private final String deferredPlacementKey; + private volatile String materialKey; private volatile Boolean air; private volatile Boolean solid; private volatile Boolean occluding; @@ -148,6 +149,17 @@ public final class ModdedBlockState implements PlatformBlockState { return namespace; } + @Override + public String materialKey() { + String cached = materialKey; + if (cached == null) { + int bracket = key.indexOf('['); + cached = bracket < 0 ? key : key.substring(0, bracket); + materialKey = cached; + } + return cached; + } + @Override public boolean isAir() { Boolean cached = air; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java new file mode 100644 index 000000000..3f6090d80 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionMetadata.java @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisRegion; +import net.minecraft.resources.Identifier; + +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +final class ModdedDimensionMetadata { + private ModdedDimensionMetadata() { + } + + static DimensionMetadata dimensionMetadata(IrisDimension dimension) { + int minY = dimension.getMinHeight(); + int maxY = dimension.getMaxHeight(); + if (maxY <= minY) { + throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey() + + "' has invalid height range " + minY + ".." + maxY); + } + return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight()); + } + + static Set collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) { + LinkedHashSet keys = new LinkedHashSet<>( + collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey())); + for (IrisRegion region : dimension.getAllRegions(() -> data)) { + if (region == null) { + continue; + } + if (!region.getSeaBiomes().isEmpty()) { + keys.add("minecraft:the_void"); + } + if (!region.getShoreBiomes().isEmpty()) { + keys.add("minecraft:beach"); + } + } + return Set.copyOf(keys); + } + + static Set collectConfiguredBiomeKeys(Iterable biomes, String dimensionLoadKey) { + LinkedHashSet keys = new LinkedHashSet<>(); + String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT); + for (IrisBiome irisBiome : biomes) { + if (irisBiome == null) { + continue; + } + Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey()); + if (derivative != null) { + keys.add(derivative.toString().toLowerCase(Locale.ROOT)); + } + if (!irisBiome.isCustom()) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT)); + } + } + return Set.copyOf(keys); + } + + static int clampSpawnHeight(int minY, int height) { + int minimum = minY + 1; + int maximum = minY + height - 2; + return Math.max(minimum, Math.min(maximum, 96)); + } + + record DimensionMetadata(int minY, int maxY, int seaLevel) { + int depth() { + return maxY - minY; + } + } + + record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java index bdf7cbc99..8c2689b9a 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDimensionRegistryStore.java @@ -50,8 +50,12 @@ public final class ModdedDimensionRegistryStore { } static List load(Path file) { + return contents(file).dimensions(); + } + + private static Contents contents(Path file) { if (!Files.isRegularFile(file)) { - return new ArrayList<>(); + return new Contents(new ArrayList<>(), new ArrayList<>()); } try { JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8)); @@ -60,7 +64,9 @@ public final class ModdedDimensionRegistryStore { throw new IllegalArgumentException("registry root has no dimensions array"); } Map deduplicated = new LinkedHashMap<>(); + List unparsed = new ArrayList<>(); for (int index = 0; index < entries.length(); index++) { + Object raw = entries.opt(index); try { JSONObject entry = entries.getJSONObject(index); String id = required(entry, "id", index, file); @@ -72,14 +78,18 @@ public final class ModdedDimensionRegistryStore { PersistentDimension previous = deduplicated.putIfAbsent( id, new PersistentDimension(id, pack, dimension, entry.getLong("seed"))); if (previous != null) { - throw new IllegalArgumentException("duplicate id '" + id + "'"); + LOGGER.warn("Iris persistent dimension registry entry {} in {} duplicates id '{}'; keeping the first", + index, file, id); } } catch (RuntimeException invalidEntry) { - LOGGER.error("Iris persistent dimension registry entry {} in {} is invalid; skipping only that entry", - index, file, invalidEntry); + if (raw != null) { + unparsed.add(raw); + } + LOGGER.warn("Iris persistent dimension registry entry {} in {} is invalid ({}); kept verbatim: {}", + index, file, invalidEntry.getMessage(), raw); } } - return new ArrayList<>(deduplicated.values()); + return new Contents(new ArrayList<>(deduplicated.values()), unparsed); } catch (RuntimeException | IOException e) { throw new IllegalStateException("Iris persistent dimension registry at " + file + " could not be read; refusing to discard persistent worlds", e); @@ -91,15 +101,19 @@ public final class ModdedDimensionRegistryStore { } public static synchronized void put(MinecraftServer server, PersistentDimension dimension) { - Map current = index(load(server)); + Path file = storeFile(server); + Contents contents = contents(file); + Map current = index(contents.dimensions()); current.put(dimension.id(), dimension); - write(server, new ArrayList<>(current.values())); + write(file, new ArrayList<>(current.values()), contents.unparsed()); } public static synchronized void remove(MinecraftServer server, String id) { - Map current = index(load(server)); + Path file = storeFile(server); + Contents contents = contents(file); + Map current = index(contents.dimensions()); if (current.remove(id) != null) { - write(server, new ArrayList<>(current.values())); + write(file, new ArrayList<>(current.values()), contents.unparsed()); } } @@ -119,11 +133,11 @@ public final class ModdedDimensionRegistryStore { return value; } - private static void write(MinecraftServer server, List dimensions) { - write(storeFile(server), dimensions); + static void write(Path file, List dimensions) { + write(file, dimensions, List.of()); } - static void write(Path file, List dimensions) { + private static void write(Path file, List dimensions, List unparsed) { JSONArray entries = new JSONArray(); for (PersistentDimension dimension : dimensions) { JSONObject entry = new JSONObject(); @@ -133,6 +147,9 @@ public final class ModdedDimensionRegistryStore { entry.put("seed", dimension.seed()); entries.put(entry); } + for (Object entry : unparsed) { + entries.put(entry); + } JSONObject root = new JSONObject(); root.put("dimensions", entries); Path temp = file.resolveSibling(FILE_NAME + ".tmp"); @@ -167,4 +184,7 @@ public final class ModdedDimensionRegistryStore { public record PersistentDimension(String id, String pack, String dimension, long seed) { } + + private record Contents(List dimensions, List unparsed) { + } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java new file mode 100644 index 000000000..845e03f46 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBinding.java @@ -0,0 +1,77 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +final class ModdedEngineBinding { + private final long timeout; + private final TimeUnit timeoutUnit; + private volatile CompletableFuture future = new CompletableFuture<>(); + + ModdedEngineBinding(long timeout, TimeUnit timeoutUnit) { + this.timeout = timeout; + this.timeoutUnit = timeoutUnit; + } + + T await(String dimensionKey) { + try { + return future.get(timeout, timeoutUnit); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for Iris generator '" + + dimensionKey + "' to bind", error); + } catch (ExecutionException error) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", + error.getCause()); + } catch (TimeoutException error) { + throw new IllegalStateException("Timed out waiting for Iris generator '" + + dimensionKey + "' to bind", error); + } + } + + void complete(T value) { + future.complete(value); + } + + void fail(Throwable error) { + future.completeExceptionally(error); + } + + void throwIfFailed(String dimensionKey) { + CompletableFuture current = future; + if (!current.isCompletedExceptionally()) { + return; + } + try { + current.join(); + } catch (CompletionException error) { + throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind", + error.getCause()); + } + } + + void reset() { + future = new CompletableFuture<>(); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java index e0597d393..29a0403e0 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEntitySpawner.java @@ -342,7 +342,7 @@ public final class ModdedEntitySpawner { double addition = RNG.r.d(); double subtraction = RNG.r.d(); double particleX = entity.getX() + addition - subtraction + RNG.r.d(); - double particleY = entity.getY() + 0.25 + addition - subtraction + level.getMinY() + RNG.r.i(effect.getParticleOffset()); + double particleY = entity.getY() + 0.25 + addition - subtraction + RNG.r.i(effect.getParticleOffset()); double particleZ = entity.getZ() + addition - subtraction + RNG.r.d(); double altX = effect.isRandomAltX() ? RNG.r.d(-effect.getParticleAltX(), effect.getParticleAltX()) : effect.getParticleAltX(); double altY = effect.isRandomAltY() ? RNG.r.d(-effect.getParticleAltY(), effect.getParticleAltY()) : effect.getParticleAltY(); 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 a72ea2264..c95af51f0 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 @@ -55,6 +55,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Stream; @@ -63,6 +64,7 @@ public final class ModdedForcedDatapack { private static final String PACK_ID = "iris_worldgen"; private static final String PACK_FOLDER = "iris"; private static final Object LOCK = new Object(); + private static final AtomicBoolean LOADED = new AtomicBoolean(false); private ModdedForcedDatapack() { } @@ -71,9 +73,26 @@ public final class ModdedForcedDatapack { return (Consumer consumer) -> { Pack pack = buildPack(); consumer.accept(pack); + LOADED.set(true); }; } + public static void verifyInjected() { + if (LOADED.get()) { + return; + } + Path packsRoot = packsRoot(); + File[] packs = packsRoot.toFile().listFiles(File::isDirectory); + if (packs == null || packs.length == 0) { + return; + } + LOGGER.error("==============================================================="); + LOGGER.error("Iris forced datapack '{}' was never loaded by this server.", PACK_ID); + LOGGER.error("{} installed pack(s) at {} contributed no dimension types or custom biomes.", packs.length, packsRoot); + LOGGER.error("Datapack source injection failed for this loader (mixin/event not applied), so world creation will fail and restarting will not fix it."); + LOGGER.error("==============================================================="); + } + public static Path datapackRoot() { return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack"); } @@ -310,7 +329,9 @@ public final class ModdedForcedDatapack { String pack, String packDimensionKey) { return registeredType.orElseThrow(() -> new IllegalStateException( "Iris dimension type '" + typeRef + "' for pack '" + pack + "' dimension '" - + packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world.")); + + packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world." + + (LOADED.get() ? "" : " The forced Iris datapack has not been loaded by this server at all" + + " (datapack source injection failed; see the Iris boot ERROR), so a restart alone will not register it."))); } private static void writeWorldPreset(KList folders, String packName, String dimensionKey, diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java new file mode 100644 index 000000000..aeb93a4d5 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedGenPool.java @@ -0,0 +1,87 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +final class ModdedGenPool { + private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger(); + private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem(); + private static volatile ExecutorService genPool = createGenPool(); + + private ModdedGenPool() { + } + + static boolean parallelChunkSystem() { + return PARALLEL_CHUNK_SYSTEM; + } + + static ExecutorService pool() { + return genPool; + } + + static void start() { + ExecutorService pool = genPool; + if (pool == null || pool.isShutdown()) { + genPool = createGenPool(); + } + } + + static void shutdown() { + ExecutorService pool = genPool; + if (pool != null) { + pool.shutdownNow(); + } + } + + private static boolean detectParallelChunkSystem() { + String[] markers = { + "com.ishland.c2me.base.ModProperties", + "com.ishland.c2me.base.common.config.C2MEConfig", + "com.ishland.c2me.opts.chunkio.ModProperties", + "ca.spottedleaf.moonrise.common.util.MoonriseCommon" + }; + for (String marker : markers) { + try { + Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader()); + return true; + } catch (Throwable ignored) { + } + } + return false; + } + + private static ExecutorService createGenPool() { + int threads = Math.max(2, Runtime.getRuntime().availableProcessors()); + ThreadPoolExecutor pool = new ThreadPoolExecutor( + threads, threads, 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + runnable -> { + Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet()); + thread.setDaemon(true); + return thread; + }); + pool.allowCoreThreadTimeOut(true); + return pool; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java index 1dc7017fd..475d7137d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java @@ -43,10 +43,11 @@ public final class ModdedIrisLog { public static void debug(String message) { if (!debugEnabled()) { + LOGGER.debug(clean(message)); return; } - LOGGER.debug(clean(message)); + LOGGER.info("[Iris/DEBUG] " + clean(message)); } public static void info(String message) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java index 434bd1729..eab4ca87d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedLootApplier.java @@ -267,15 +267,21 @@ public final class ModdedLootApplier { for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) { ItemStack existing = container.getItem(i); if (existing.isEmpty()) { - container.setItem(i, stack); - return; + container.setItem(i, stack.split(container.getMaxStackSize(stack))); + continue; } - if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) { - int move = Math.min(stack.getCount(), existing.getMaxStackSize() - existing.getCount()); - existing.grow(move); - stack.shrink(move); + if (ItemStack.isSameItemSameComponents(existing, stack)) { + int limit = container.getMaxStackSize(existing); + if (existing.getCount() < limit) { + int move = Math.min(stack.getCount(), limit - existing.getCount()); + existing.grow(move); + stack.shrink(move); + } } } + if (!stack.isEmpty()) { + IrisLogging.debug("Iris loot: container full, dropped " + stack.getCount() + "x " + stack.getItem()); + } } private static void scramble(Container container, RNG rng) { 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 new file mode 100644 index 000000000..b22a4b4b4 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructureStage.java @@ -0,0 +1,439 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; +import art.arcane.iris.engine.framework.NativeStructureStartPlan; +import art.arcane.iris.engine.object.IrisMaterialPalette; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.nativegen.NativeStructureGenerationException; +import art.arcane.iris.nativegen.NativeStructurePostProcessor; +import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope; +import art.arcane.iris.nativegen.NativeStructureSurfaceFitter; +import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator; +import art.arcane.iris.nativegen.NativeStructureVegetationClearer; +import art.arcane.iris.nativegen.NativeStructureVerticalPlacer; +import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.math.RNG; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntBinaryOperator; + +/** + * Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the + * {@link net.minecraft.world.level.chunk.ChunkGenerator} overrides because they issue {@code super} calls; + * everything they do beyond that lives here. + */ +final class ModdedNativeStructureStage { + private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096; + private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck"); + + private final IrisModdedChunkGenerator generator; + private final ConcurrentHashMap worldCheckStructureShifts = new ConcurrentHashMap<>(); + private volatile StructureStepCache structureStepCache; + + ModdedNativeStructureStage(IrisModdedChunkGenerator generator) { + this.generator = generator; + } + + Pair> findNearestIrisStructure(ServerLevel level, + HolderSet holders, + BlockPos pos, int radius, boolean findUnexplored, + Engine current) { + if (findUnexplored) { + return null; + } + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + BlockPos best = null; + Holder bestHolder = null; + long bestDistance = Long.MAX_VALUE; + for (Holder holder : holders) { + Identifier id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure locate received an unregistered structure holder"); + } + String structureId = id.toString(); + if (!IrisStructureLocator.isPlaced(current, structureId)) { + continue; + } + IrisStructureLocator.LocateResult result = IrisStructureLocator.locate( + current, structureId, pos.getX(), pos.getZ(), radius); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + throw new IllegalStateException("Iris structure locate reached its safety limit for " + + structureId + " within " + radius + " chunks"); + } + if (!result.found()) { + continue; + } + long dx = (long) result.originX() - pos.getX(); + long dz = (long) result.originZ() - pos.getZ(); + long distance = dx * dx + dz * dz; + if (distance < bestDistance) { + bestDistance = distance; + best = new BlockPos(result.originX(), result.baseY(), result.originZ()); + bestHolder = holder; + } + } + return best == null ? null : Pair.of(best, bestHolder); + } + + HolderSet filterReachableNativeStructures(ServerLevel level, HolderSet holders, + Engine current) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> kept = new ArrayList<>(holders.size()); + for (Holder holder : holders) { + Identifier id = registry.getKey(holder.value()); + if (id == null) { + throw new IllegalStateException("Native structure filtering received an unregistered structure holder"); + } + String key = id.toString(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current, + key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step())); + if (!decision.generate() || !generator.structureBiomeSource.isStructureReachable(holder)) { + continue; + } + kept.add(holder); + } + return kept.size() == holders.size() ? holders : HolderSet.direct(kept); + } + + void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk, + Map previousStarts, + Map configuredStarts, + Engine current, + StructureTemplateManager templateManager) { + Registry registry = registryAccess.lookupOrThrow(Registries.STRUCTURE); + ChunkPos chunkPos = chunk.getPos(); + for (Map.Entry entry : chunk.getAllStarts().entrySet()) { + Structure structure = entry.getKey(); + StructureStart start = entry.getValue(); + if (!start.isValid() || previousStarts.get(structure) == start) { + continue; + } + if (configuredStarts.containsKey(structure)) { + recordWorldCheckStructureShift( + configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0); + continue; + } + Identifier id = registry.getKey(structure); + String structureId = id == null ? null : id.toString(); + if (structureId == null) { + throw NativeStructureGenerationException.failure( + "resolution", null, chunkPos.x(), chunkPos.z()); + } + boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step()); + IrisNativeStructureDecision decision; + try { + decision = NativeStructureGenerationPolicy.resolve(current, + structureId, undergroundStep); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "policy resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + if (!decision.generate()) { + chunk.setStartForStructure(structure, StructureStart.INVALID_START); + continue; + } + int offsetY; + try { + offsetY = NativeStructureVerticalPlacer.applyVerticalPlacement( + start, + structureId, + decision.yShift(), + generator.getSeaLevel(), + chunk.getMinY(), + chunk.getMinY() + chunk.getHeight(), + undergroundStep, + decision.preserveSourceY(), + decision.yBand(), + (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); + StructureStart wrapped = NativeStructureReferenceEnvelope.wrap( + start, structure, start.getReferences(), templateManager, + NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain())); + chunk.setStartForStructure(structure, wrapped); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error); + } + recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY); + } + } + + void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) { + ChunkPos disabledChunk = chunk.getPos(); + throw new IllegalStateException("Iris cannot generate native structures in chunk " + + disabledChunk.x() + "," + disabledChunk.z() + + " because generate-structures=false disables them outside the pack; set generate-structures=true, " + + "restart the server, and deny individual structures through importedStructures.disabled"); + } + ChunkPos chunkPos = chunk.getPos(); + SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY()); + BlockPos origin = sectionPos.origin(); + Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> byStep = structuresByStep(registry); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); + BoundingBox area = writableArea(chunk); + int steps = GenerationStep.Decoration.values().length; + Engine current = generator.engine(); + List placementGroups = new ArrayList<>(); + List heightmapStarts = new ArrayList<>(); + List nativeStarts = new ArrayList<>(); + List vegetationTargets = new ArrayList<>(); + List terrainTargets = new ArrayList<>(); + for (int step = 0; step < steps; step++) { + int index = 0; + for (Structure structure : byStep.get(step)) { + Identifier id = registry.getKey(structure); + String structureId = id == null ? null : id.toString(); + if (structureId == null) { + throw NativeStructureGenerationException.failure( + "resolution", null, chunkPos.x(), chunkPos.z()); + } + try { + IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current, + structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + List starts = structureManager.startsForStructure(sectionPos, structure); + List resolvedPlacements = new ArrayList<>(starts.size()); + for (StructureStart start : starts) { + NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan( + current, structureId, start.getChunkPos().x(), start.getChunkPos().z()); + IrisNativeStructureDecision decision = plan == null + ? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan); + if (!decision.generate()) { + continue; + } + resolvedPlacements.add(new NativePlacement(start, decision)); + heightmapStarts.add(start); + terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget( + structureId, start, + NativeStructureTerrainIntegrator.resolveNativeTerrain( + start, decision.terrain()))); + if (plan == null || !plan.placement().isUnderground()) { + nativeStarts.add(start); + } + boolean clearEntireFootprint = NativeStructureVegetationClearer + .shouldClearEntireVegetationFootprint( + structure.step(), decision.clearVegetation()); + vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget( + start, clearEntireFootprint)); + } + if (!resolvedPlacements.isEmpty()) { + placementGroups.add(new NativePlacementGroup( + structureId, index, step, List.copyOf(resolvedPlacements))); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "resolution", structureId, chunkPos.x(), chunkPos.z(), error); + } + index++; + } + } + try { + int runtimeMinY = world.getMinY(); + WorldgenTerrainHeightmaps.primeStructurePlacement( + world, heightmapStarts, + worldgenSurfaceHeight(current, runtimeMinY), + worldgenFloorHeight(current, runtimeMinY)); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "heightmap priming", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world, area, nativeStarts, + (x, z) -> current.getHeight(x, z, true) + current.getMinHeight()); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain integration", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructureVegetationClearer.clearIntersectingVegetation( + world, chunk, area, vegetationTargets); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "vegetation cleanup", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + try { + NativeStructurePostProcessor.prepareTerrain( + world, area, terrainTargets, this::resolvePaletteBlock); + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "terrain carving", nativeStructureBatchContext(placementGroups), + chunkPos.x(), chunkPos.z(), error); + } + for (NativePlacementGroup group : placementGroups) { + random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step()); + try { + for (NativePlacement placement : group.placements()) { + placeVanillaStructure(world, structureManager, random, area, chunkPos, + group.structureId(), placement.start(), placement.decision()); + } + } catch (Throwable error) { + throw NativeStructureGenerationException.failure( + "placement", group.structureId(), chunkPos.x(), chunkPos.z(), error); + } + } + } + + private static String nativeStructureBatchContext(List placementGroups) { + if (placementGroups.isEmpty()) { + return ""; + } + StringBuilder context = new StringBuilder("["); + for (int i = 0; i < placementGroups.size(); i++) { + if (i > 0) { + context.append(", "); + } + context.append(placementGroups.get(i).structureId()); + } + return context.append(']').toString(); + } + + private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, + WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, + String structureId, StructureStart start, + IrisNativeStructureDecision decision) { + NativeStructurePostProcessor.place(world, structureManager, generator, random, area, chunkPos, + structureId, start, decision, this::resolvePaletteBlock, + (x, z) -> generator.engine().getHeight(x, z, true) + generator.engine().getMinHeight()); + } + + private List> structuresByStep(Registry registry) { + StructureStepCache cached = structureStepCache; + if (cached != null && cached.registry() == registry) { + return cached.structures(); + } + synchronized (generator) { + cached = structureStepCache; + if (cached != null && cached.registry() == registry) { + return cached.structures(); + } + int steps = GenerationStep.Decoration.values().length; + List> grouped = new ArrayList<>(steps); + for (int step = 0; step < steps; step++) { + grouped.add(new ArrayList<>()); + } + for (Structure structure : registry) { + grouped.get(structure.step().ordinal()).add(structure); + } + for (int step = 0; step < steps; step++) { + grouped.set(step, List.copyOf(grouped.get(step))); + } + List> resolved = List.copyOf(grouped); + structureStepCache = new StructureStepCache(registry, resolved); + return resolved; + } + } + + private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) { + if (!WORLD_CHECK_ENABLED || structureId == null) { + return; + } + if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) { + worldCheckStructureShifts.clear(); + } + worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY); + } + + Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) { + if (structureId == null || startChunk == null) { + return null; + } + return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack())); + } + + void clearWorldCheckStructureShifts() { + worldCheckStructureShifts.clear(); + } + + private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng, + int x, int y, int z) { + PlatformBlockState platformState = palette.get(rng, x, y, z, generator.engine().getData()); + if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) { + throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at " + + x + "," + y + "," + z); + } + return blockState; + } + + private BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + int minX = chunkPos.getMinBlockX(); + int minZ = chunkPos.getMinBlockZ(); + int minY = chunk.getMinY(); + int maxY = minY + chunk.getHeight() - 1; + return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15); + } + + private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) { + return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1; + } + + private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) { + return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1; + } + + private record NativeStructureStartKey(String structureId, long chunkPosition) { + } + + private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) { + } + + private record NativePlacementGroup(String structureId, int featureIndex, int step, + List placements) { + } + + private record StructureStepCache(Registry registry, List> structures) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java index 3ccece584..15584a3c1 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java @@ -26,6 +26,7 @@ import art.arcane.iris.spi.PlatformEntityType; import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.spi.PlatformScheduler; import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformWorld; import net.minecraft.core.BlockPos; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; @@ -141,8 +142,8 @@ public final class ModdedPlatform implements IrisPlatform { } @Override - public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) { - if (!(world instanceof ServerLevel level) || entityKey == null) { + public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) { + if (world == null || entityKey == null || !(world.nativeHandle() instanceof ServerLevel level)) { return false; } PlatformEntityType resolved = registries.entity(entityKey); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java new file mode 100644 index 000000000..17fe820dc --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedSpawnTableMerger.java @@ -0,0 +1,119 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.util.project.context.IrisContext; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; + +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the + * generator monitor because the repoint/bind/reset paths already hold it while resetting this state. + */ +final class ModdedSpawnTableMerger { + private final IrisModdedChunkGenerator generator; + private final ConcurrentHashMap> vanillaSpawnBiomes = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> mergedSpawnTables = new ConcurrentHashMap<>(); + private volatile boolean vanillaSpawnBiomesInitialized; + + ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) { + this.generator = generator; + } + + void initializeVanillaSpawnBiomes(Registry registry) { + synchronized (generator) { + if (vanillaSpawnBiomesInitialized) { + return; + } + Engine current = generator.engineOrNull(); + if (current == null) { + return; + } + + try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes"); + IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { + String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT); + for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) { + if (irisBiome == null || !irisBiome.isCustom()) { + continue; + } + Holder vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey()); + if (vanillaHolder == null) { + continue; + } + for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { + Holder customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId()); + if (customHolder != null) { + vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); + } + } + } + vanillaSpawnBiomesInitialized = true; + } + } + } + + Holder vanillaSpawnBiome(Biome biome) { + return vanillaSpawnBiomes.get(biome); + } + + WeightedList mergedSpawnTable( + Biome biome, MobCategory category, + WeightedList vanillaSpawns, + WeightedList explicitSpawns) { + SpawnTableKey key = new SpawnTableKey(biome, category); + return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)); + } + + private Holder resolveBiomeHolder(Registry registry, String key) { + if (key == null || key.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + return null; + } + Optional> reference = registry.get(identifier); + return reference.>map((Holder.Reference value) -> value).orElse(null); + } + + void resetVanillaSpawnBiomes() { + synchronized (generator) { + vanillaSpawnBiomes.clear(); + mergedSpawnTables.clear(); + vanillaSpawnBiomesInitialized = false; + } + } + + private record SpawnTableKey(Biome biome, MobCategory category) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java index 780f6a282..d4d4b91f5 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java @@ -73,6 +73,7 @@ public final class ModdedStartup { if (!STARTED.compareAndSet(false, true)) { return; } + ModdedForcedDatapack.verifyInjected(); reinjectPersistentDimensions(server); ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); @@ -88,6 +89,8 @@ public final class ModdedStartup { File[] packDirs = packsRoot.listFiles(File::isDirectory); PackValidationRegistry.clear(); if (packDirs == null || packDirs.length == 0) { + LOGGER.info("Iris found no packs to validate under {}; install one with /iris download ", + packsRoot.getAbsolutePath()); return; } for (File packDir : packDirs) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java index 7fb8f9625..c24db196a 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStateRotator.java @@ -182,13 +182,28 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator private static Property findRotation(BlockState state) { for (Property property : state.getProperties()) { - if (property.getName().equals("rotation") && property instanceof IntegerProperty) { + if (property.getName().equals("rotation") + && property instanceof IntegerProperty integer + && isFullRotationCycle(integer)) { return property; } } return null; } + private static boolean isFullRotationCycle(IntegerProperty property) { + List values = property.getPossibleValues(); + if (values.size() != ROTATION_CYCLE_MODS.length) { + return false; + } + for (int value : values) { + if (value < 0 || value >= ROTATION_CYCLE_MODS.length) { + return false; + } + } + return true; + } + private static Property findAxis(BlockState state) { for (Property property : state.getProperties()) { if (property.getName().equals("axis") && property.getValueClass() == Direction.Axis.class) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java index 2a1780f22..b0a3e4915 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedTileReader.java @@ -41,7 +41,7 @@ import java.util.Locale; import java.util.function.Supplier; public final class ModdedTileReader implements TileData.TileReader { - private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create(); + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create(); private static final int DYE_COLOR_COUNT = 16; private static final Identifier DEFAULT_SPAWNER_ENTITY = Identifier.parse("minecraft:pig"); private static final Identifier DEFAULT_BANNER_PATTERN = Identifier.parse("minecraft:base"); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java index 154130e41..20a18a5bf 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java @@ -18,83 +18,35 @@ package art.arcane.iris.modded; -import art.arcane.iris.core.nms.datapack.DataVersion; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; -import art.arcane.iris.engine.framework.StructureVerticalBounds; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisNativeStructureDecision; -import art.arcane.iris.nativegen.NativeStructurePostProcessor; -import art.arcane.volmlib.util.json.JSONObject; -import com.mojang.datafixers.util.Pair; +import art.arcane.iris.modded.WorldCheckStructureAudit.NativeStructureGate; +import art.arcane.iris.modded.WorldCheckStructureAudit.PendingVillagePoi; +import art.arcane.iris.modded.WorldCheckStructureAudit.PoiAudit; import net.minecraft.core.BlockPos; -import net.minecraft.core.Holder; -import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.resources.Identifier; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import net.minecraft.tags.BlockTags; -import net.minecraft.world.entity.ai.village.poi.PoiManager; -import net.minecraft.world.entity.ai.village.poi.PoiRecord; -import net.minecraft.world.entity.item.ItemEntity; -import net.minecraft.world.item.Items; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; -import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunkSection; -import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.level.levelgen.structure.BoundingBox; -import net.minecraft.world.level.levelgen.structure.Structure; -import net.minecraft.world.level.levelgen.structure.StructurePiece; -import net.minecraft.world.level.levelgen.structure.StructureStart; -import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; -import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; -import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Comparator; import java.util.HexFormat; import java.util.LinkedHashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; -import java.util.function.IntBinaryOperator; public final class ModdedWorldCheck { private static final int EXIT_PASS = 0; private static final int EXIT_FAILURE = 1; - private static final int MAX_FOOTPRINT_CHUNKS = 96; - private static final int MAX_START_REFERENCE_CHUNKS = 16; - private static final int MAX_STRUCTURE_CANDIDATES = 1024; private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L; private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L; - private static final List STRUCTURE_CHECKS = List.of( - new StructureCheck("stronghold", List.of("minecraft:stronghold"), 256), - new StructureCheck("trial_chambers", List.of("minecraft:trial_chambers"), 128), - new StructureCheck("mansion", List.of("minecraft:mansion"), 256), - new StructureCheck("village", List.of( - "minecraft:village_plains", - "minecraft:village_desert", - "minecraft:village_savanna", - "minecraft:village_snowy", - "minecraft:village_taiga"), 128), - new StructureCheck("monument", List.of("minecraft:monument"), 128) - ); private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final ProcessExit PROCESS_EXIT = Runtime.getRuntime()::exit; private static volatile MinecraftServer startedServer; @@ -220,12 +172,13 @@ public final class ModdedWorldCheck { if (!irisGenerator) { LOGGER.error("[worldcheck] {} is NOT using IrisModdedChunkGenerator", levelId); } - boolean dimensionTypeOk = generator != null && checkDimensionType(level, generator); + boolean dimensionTypeOk = generator != null + && WorldCheckDimensionContract.checkDimensionType(level, generator); BlockPos spawn = level.getRespawnData().pos(); LOGGER.info("[worldcheck] spawn: {} {} {} (minY={} height={})", spawn.getX(), spawn.getY(), spawn.getZ(), level.getMinY(), level.getHeight()); - MessageDigest digest = sha256(); + MessageDigest digest = WorldCheckPredicates.sha256(); List samples = new ArrayList<>(); Set surfaceKeys = new LinkedHashSet<>(); for (int dx = 0; dx < 4; dx++) { @@ -276,10 +229,10 @@ public final class ModdedWorldCheck { LOGGER.error("[worldcheck] generated terrain has no block variety (flat-world signature)"); } - boolean entityMixinsOk = checkEntityMixins(level); + boolean entityMixinsOk = WorldCheckDimensionContract.checkEntityMixins(level); NativeStructureGate structureGate = generator == null ? new NativeStructureGate(false, 0, false, null) - : checkNativeStructures(level, generator, spawn); + : WorldCheckStructureAudit.checkNativeStructures(level, generator, spawn); boolean terrainOk = sectionsOk && varietyOk; boolean nonStructurePass = irisGenerator && dimensionTypeOk && terrainOk && entityMixinsOk; return new WorldCheckPreparation(nonStructurePass, terrainOk, dimensionTypeOk, @@ -291,961 +244,34 @@ public final class ModdedWorldCheck { boolean poiOk = false; PendingVillagePoi pendingPoi = structureGate.pendingPoi(); if (pendingPoi != null) { - PoiAudit poi = auditStructurePois(pendingPoi.level(), pendingPoi.start()); - poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds()); - qaEvent("village_poi_metric", "village", poiOk, + PoiAudit poi = WorldCheckStructureAudit.auditStructurePois(pendingPoi.level(), pendingPoi.start()); + poiOk = WorldCheckPredicates.villagePoiPass(poi.inBounds(), poi.outOfBounds()); + WorldCheckPredicates.qaEvent("village_poi_metric", "village", poiOk, "inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds()); if (!poiOk) { LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}", poi.inBounds(), poi.outOfBounds()); } } else { - qaEvent("village_poi_metric", "village", false, "skipped=structure"); + WorldCheckPredicates.qaEvent("village_poi_metric", "village", false, "skipped=structure"); } int passed = structureGate.nonVillagePassed() + (structureGate.villagePassBeforePoi() && poiOk ? 1 : 0); boolean structurePass = structureGate.passBeforePoi() && poiOk; - LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size()); - qaEvent("structure_aggregate", "all", structurePass, - "passed=" + passed + ",total=" + STRUCTURE_CHECKS.size()); + LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, + WorldCheckStructureAudit.STRUCTURE_CHECKS.size()); + WorldCheckPredicates.qaEvent("structure_aggregate", "all", structurePass, + "passed=" + passed + ",total=" + WorldCheckStructureAudit.STRUCTURE_CHECKS.size()); boolean pass = preparation.nonStructurePass() && structurePass; LOGGER.info("[worldcheck] {}", pass ? "PASS" : "FAIL"); - qaEvent("worldcheck_final", "all", pass, - "structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + preparation.terrainOk() + WorldCheckPredicates.qaEvent("worldcheck_final", "all", pass, + "structures=" + WorldCheckStructureAudit.STRUCTURE_CHECKS.size() + + ",terrain=" + preparation.terrainOk() + ",dimensionType=" + preparation.dimensionTypeOk() + ",entityMixins=" + preparation.entityMixinsOk()); return pass; } - private static boolean checkDimensionType(ServerLevel level, IrisModdedChunkGenerator generator) { - try { - IrisDimension dimension = generator.commandEngine().getDimension(); - DimensionContract expected = expectedDimensionContract(dimension); - DimensionContract actual = runtimeDimensionContract(level.dimensionType()); - boolean pass = matchesDimensionContract(level.getMinY(), level.getHeight(), expected, actual); - String detail = "expected=" + expected + ",actual=" + actual - + ",levelMinY=" + level.getMinY() + ",levelHeight=" + level.getHeight(); - qaEvent("dimension_type", dimension.getLoadKey(), pass, detail); - if (!pass) { - LOGGER.error("[worldcheck] dimension type mismatch for {}: {}", dimension.getLoadKey(), detail); - } else { - LOGGER.info("[worldcheck] dimension type contract: {}", detail); - } - return pass; - } catch (Throwable error) { - LOGGER.error("[worldcheck] could not validate the Iris dimension type contract", error); - qaEvent("dimension_type", generator.activeDimensionKey(), false, - "validationError=" + error.getClass().getSimpleName() + ":" + error.getMessage()); - return false; - } - } - - static DimensionContract expectedDimensionContract(IrisDimension dimension) { - JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get())); - return new DimensionContract( - json.getInt("min_y"), - json.getInt("height"), - json.getInt("logical_height"), - json.getDouble("coordinate_scale"), - (float) json.getDouble("ambient_light"), - json.getBoolean("has_skylight"), - json.getBoolean("has_ceiling"), - json.getBoolean("has_ender_dragon_fight"), - json.getInt("monster_spawn_block_light_limit")); - } - - static DimensionContract runtimeDimensionContract(DimensionType dimensionType) { - return new DimensionContract( - dimensionType.minY(), - dimensionType.height(), - dimensionType.logicalHeight(), - dimensionType.coordinateScale(), - dimensionType.ambientLight(), - dimensionType.hasSkyLight(), - dimensionType.hasCeiling(), - dimensionType.hasEnderDragonFight(), - dimensionType.monsterSpawnBlockLightLimit()); - } - - static boolean matchesDimensionContract(int levelMinY, int levelHeight, - DimensionContract expected, DimensionContract actual) { - return levelMinY == expected.minY() - && levelHeight == expected.height() - && actual.equals(expected); - } - - private static boolean checkEntityMixins(ServerLevel level) { - ItemEntity item = new ItemEntity(level, 0D, level.getMinY(), 0D, Items.COBBLESTONE.getDefaultInstance()); - boolean vanillaSave = item.shouldBeSaved(); - ModdedEntityPersistence.configure(item, false); - boolean suppressed = !item.shouldBeSaved(); - ModdedEntityPersistence.configure(item, true); - boolean restored = item.shouldBeSaved(); - boolean pass = vanillaSave && suppressed && restored; - qaEvent("entity_mixin", "persistence", pass, - "vanilla=" + vanillaSave + ",suppressed=" + suppressed + ",restored=" + restored); - if (!pass) { - LOGGER.error("[worldcheck] shared entity mixins are not active on this loader"); - } - return pass; - } - - private static NativeStructureGate checkNativeStructures(ServerLevel level, - IrisModdedChunkGenerator generator, - BlockPos origin) { - boolean pass = true; - int nonVillagePassed = 0; - boolean villagePass = false; - PendingVillagePoi pendingPoi = null; - for (StructureCheck check : STRUCTURE_CHECKS) { - StructureCheckResult result = checkNativeStructure(level, generator, origin, check); - if (!result.pass()) { - pass = false; - } - if (check.label().equals("village")) { - villagePass = result.pass(); - pendingPoi = result.pendingPoi(); - } else if (result.pass()) { - nonVillagePassed++; - } - } - return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi); - } - - private static StructureCheckResult checkNativeStructure(ServerLevel level, - IrisModdedChunkGenerator generator, - BlockPos origin, - StructureCheck check) { - Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); - List> registered = new ArrayList<>(check.registryKeys().size()); - LinkedHashSet registeredKeys = new LinkedHashSet<>(); - for (String key : check.registryKeys()) { - Identifier identifier = Identifier.tryParse(key); - if (identifier == null) { - continue; - } - Optional> resolved = registry.get(identifier); - if (resolved.isPresent()) { - registered.add(resolved.get()); - registeredKeys.add(identifier.toString()); - } - } - boolean registryOk = registered.size() == check.registryKeys().size(); - LOGGER.info("[worldcheck] {} registry: {}/{} resolved {}", check.label(), registered.size(), - check.registryKeys().size(), registeredKeys); - qaEvent("structure_registry", check.label(), registryOk, - "resolved=" + registered.size() + ",expected=" + check.registryKeys().size() - + ",keys=" + String.join("|", registeredKeys)); - if (!registryOk) { - LOGGER.error("[worldcheck] {} registry resolution failed; expected {}", check.label(), check.registryKeys()); - emitSkipped(check, "registry", "structure_reachability", "structure_locate", - "structure_start_reference", "structure_footprint", "structure_material", - "structure_block_entity"); - return new StructureCheckResult(false, null); - } - - List> reachable = new ArrayList<>(registered.size()); - LinkedHashSet reachableKeys = new LinkedHashSet<>(); - for (Holder holder : registered) { - if (!generator.isNativeStructureReachable(holder)) { - continue; - } - reachable.add(holder); - Identifier key = registry.getKey(holder.value()); - if (key != null) { - reachableKeys.add(key.toString()); - } - } - boolean reachableOk = !reachable.isEmpty(); - LOGGER.info("[worldcheck] {} biome-reachable through Iris: {}", check.label(), reachableKeys); - qaEvent("structure_reachability", check.label(), reachableOk, - "reachable=" + reachable.size() + ",registered=" + registered.size() - + ",keys=" + String.join("|", reachableKeys)); - if (!reachableOk) { - LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label()); - emitSkipped(check, "reachability", "structure_locate", "structure_start_reference", - "structure_footprint", "structure_material", "structure_block_entity"); - return new StructureCheckResult(false, null); - } - - long locateStart = System.nanoTime(); - Pair> found = findGeneratedStructureCandidate( - level, reachable, origin, check.locateRadius()); - long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L; - Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value()); - boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString()); - qaEvent("structure_locate", check.label(), locateOk, - "method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius() - + ",result=" + (foundKey == null ? "none" : foundKey)); - if (found == null) { - LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms", - check.label(), check.locateRadius(), locateMillis); - emitSkipped(check, "locate", "structure_start_reference", "structure_footprint", - "structure_material", "structure_block_entity"); - return new StructureCheckResult(false, null); - } - - BlockPos position = found.getFirst(); - LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})", - check.label(), position.getX(), position.getY(), position.getZ(), locateMillis, - check.locateRadius(), foundKey); - if (!locateOk) { - LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey); - emitSkipped(check, "locate", "structure_start_reference", "structure_footprint", - "structure_material", "structure_block_entity"); - return new StructureCheckResult(false, null); - } - - int chunkX = position.getX() >> 4; - int chunkZ = position.getZ() >> 4; - ChunkAccess targetChunk = level.getChunk(chunkX, chunkZ); - Structure structure = found.getSecond().value(); - StructureStart start = resolveStructureStart(level, targetChunk, structure); - boolean validStart = start != null && start.isValid(); - int references = targetChunk.getReferencesForStructure(structure).size(); - boolean startReferenceOk = hasNativeStructureEvidence(validStart, references); - LOGGER.info("[worldcheck] {} target chunk {},{}: valid start={}, references={}", - check.label(), chunkX, chunkZ, validStart, references); - qaEvent("structure_start_reference", check.label(), startReferenceOk, - "chunk=" + chunkX + "," + chunkZ + ",validStart=" + validStart - + ",references=" + references); - if (!startReferenceOk || !validStart) { - LOGGER.error("[worldcheck] {} located at chunk {},{} but no resolvable valid start was generated", - check.label(), chunkX, chunkZ); - emitSkipped(check, "start_reference", "structure_footprint", "structure_material", - "structure_block_entity"); - return new StructureCheckResult(false, null); - } - - IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( - generator.commandEngine(), foundKey.toString(), - NativeStructurePostProcessor.isUndergroundStep(structure.step())); - Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos()); - BoundingBox shiftedBounds = start.getBoundingBox(); - boolean verticalShiftOk = verticalShiftMatches( - decision.yShift(), appliedShift, shiftedBounds.minY(), shiftedBounds.maxY(), - level.getMinY(), level.getMaxY()); - qaEvent("structure_vertical_shift", check.label(), verticalShiftOk, - "configured=" + decision.yShift() + ",applied=" - + (appliedShift == null ? "unrecorded" : appliedShift)); - if (!verticalShiftOk) { - LOGGER.error("[worldcheck] {} expected vertical shift {} but generation recorded {}", - check.label(), decision.yShift(), appliedShift); - } - - FootprintAudit footprint = auditFootprint(level, structure, start, check, foundKey); - boolean footprintOk = footprint.inspectedChunks() > 0 - && footprint.evidenceChunks() == footprint.inspectedChunks() - && footprint.coveredPieces() == footprint.totalPieces(); - LOGGER.info("[worldcheck] {} footprint: chunks={}/{} evidence={} pieces={}/{}", - check.label(), footprint.inspectedChunks(), footprint.availableChunks(), - footprint.evidenceChunks(), footprint.coveredPieces(), footprint.totalPieces()); - qaEvent("structure_footprint", check.label(), footprintOk, - "inspected=" + footprint.inspectedChunks() + ",available=" + footprint.availableChunks() - + ",evidence=" + footprint.evidenceChunks() + ",coveredPieces=" - + footprint.coveredPieces() + ",totalPieces=" + footprint.totalPieces()); - - boolean materialOk = hasCharacteristicMaterialEvidence(footprint.characteristicBlocks(), - footprint.characteristicChunks(), footprint.materialScannedChunks()); - LOGGER.info("[worldcheck] {} material: blocks={} chunks={}/{}", - check.label(), footprint.characteristicBlocks(), footprint.characteristicChunks(), - footprint.materialScannedChunks()); - qaEvent("structure_material", check.label(), materialOk, - "blocks=" + footprint.characteristicBlocks() + ",chunks=" + footprint.characteristicChunks() - + ",scanned=" + footprint.materialScannedChunks()); - - boolean vegetationOk = true; - if (check.label().equals("mansion")) { - boolean overlap = footprint.vegetationBlocks() > 0; - vegetationOk = mansionVegetationPass(footprint.vegetationBlocks()); - LOGGER.info("[worldcheck] mansion vegetation metric: remaining log/leaf blocks={} columns={} overlap={}", - footprint.vegetationBlocks(), footprint.vegetationColumns(), overlap); - qaEvent("mansion_vegetation_metric", check.label(), vegetationOk, - "remainingLogsOrLeaves=" + footprint.vegetationBlocks() + ",columns=" - + footprint.vegetationColumns() + ",overlap=" + overlap); - } - - boolean foundationOk = true; - PendingVillagePoi pendingPoi = null; - if (check.label().equals("village")) { - foundationOk = villageFoundationPass(footprint.foundationGapColumns()); - LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}", - footprint.foundationBaseColumns(), footprint.foundationBlocks(), - footprint.foundationColumns(), footprint.foundationGapColumns()); - qaEvent("village_foundation_metric", check.label(), foundationOk, - "bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase=" - + footprint.foundationBlocks() + ",columns=" - + footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns()); - pendingPoi = new PendingVillagePoi(level, start); - } - - boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent(); - LOGGER.info("[worldcheck] {} block entities: state blocks={}, present={}, missing={}", - check.label(), footprint.blockEntityStates(), footprint.blockEntitiesPresent(), - footprint.blockEntityStates() - footprint.blockEntitiesPresent()); - qaEvent("structure_block_entity", check.label(), blockEntityOk, - "states=" + footprint.blockEntityStates() + ",present=" + footprint.blockEntitiesPresent() - + ",missing=" + (footprint.blockEntityStates() - footprint.blockEntitiesPresent())); - if (!footprintOk) { - LOGGER.error("[worldcheck] {} structure footprint is incomplete", check.label()); - } - if (!materialOk) { - LOGGER.error("[worldcheck] {} has no distributed characteristic structure material", check.label()); - } - if (!blockEntityOk) { - LOGGER.error("[worldcheck] {} generated block-entity states without matching block entities", check.label()); - } - if (!vegetationOk) { - LOGGER.error("[worldcheck] mansion vegetation still intersects the generated structure footprint"); - } - if (!foundationOk) { - LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement"); - } - boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk - && vegetationOk && foundationOk; - return new StructureCheckResult(pass, pass ? pendingPoi : null); - } - - private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk, - Structure structure) { - StructureStart direct = targetChunk.getStartForStructure(structure); - if (direct != null && direct.isValid()) { - return direct; - } - int checked = 0; - for (long packed : targetChunk.getReferencesForStructure(structure)) { - if (checked++ >= MAX_START_REFERENCE_CHUNKS) { - break; - } - ChunkAccess referencedChunk = level.getChunk(ChunkPos.getX(packed), ChunkPos.getZ(packed)); - StructureStart referenced = referencedChunk.getStartForStructure(structure); - if (referenced != null && referenced.isValid()) { - return referenced; - } - } - return direct; - } - - private static Pair> findGeneratedStructureCandidate( - ServerLevel level, List> structures, BlockPos origin, int maxRadius) { - ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); - Set attempted = new LinkedHashSet<>(); - for (Holder structure : structures) { - for (StructurePlacement placement : state.getPlacementsForStructure(structure)) { - if (!(placement instanceof ConcentricRingsStructurePlacement rings)) { - continue; - } - List positions = state.getRingPositionsFor(rings); - if (positions == null) { - continue; - } - List sorted = new ArrayList<>(positions); - sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position))); - for (ChunkPos position : sorted) { - Pair> found = inspectStructureCandidate( - level, structures, placement, position, attempted); - if (found != null) { - return found; - } - if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) { - return null; - } - } - } - } - - int originChunkX = origin.getX() >> 4; - int originChunkZ = origin.getZ() >> 4; - for (int radius = 0; radius <= maxRadius; radius++) { - for (Holder structure : structures) { - for (StructurePlacement placement : state.getPlacementsForStructure(structure)) { - if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) { - continue; - } - for (int x = -radius; x <= radius; x++) { - boolean xEdge = x == -radius || x == radius; - for (int z = -radius; z <= radius; z++) { - if (!xEdge && z != -radius && z != radius) { - continue; - } - int sectorX = originChunkX + randomSpread.spacing() * x; - int sectorZ = originChunkZ + randomSpread.spacing() * z; - ChunkPos candidate = randomSpread.getPotentialStructureChunk( - state.getLevelSeed(), sectorX, sectorZ); - if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) { - continue; - } - Pair> found = inspectStructureCandidate( - level, structures, placement, candidate, attempted); - if (found != null) { - return found; - } - if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) { - return null; - } - } - } - } - } - } - return null; - } - - private static Pair> inspectStructureCandidate( - ServerLevel level, List> structures, StructurePlacement placement, - ChunkPos candidate, Set attempted) { - if (!attempted.add(candidate.pack())) { - return null; - } - ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z()); - for (Holder structure : structures) { - StructureStart start = resolveStructureStart(level, chunk, structure.value()); - if (start == null || !start.isValid()) { - continue; - } - BlockPos locate = placement.getLocatePos(start.getChunkPos()); - BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ()); - return Pair.of(resolved, structure); - } - return null; - } - - private static long distanceSquared(BlockPos origin, ChunkPos position) { - long x = (long) position.getMinBlockX() - origin.getX(); - long z = (long) position.getMinBlockZ() - origin.getZ(); - return x * x + z * z; - } - - private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start, - StructureCheck check, Identifier structureKey) { - List pieces = start.getPieces(); - BoundingBox bounds = start.getBoundingBox(); - int availableChunks = footprintChunkCount(bounds); - List selected = selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS); - int evidenceChunks = 0; - int blockEntityStates = 0; - int blockEntitiesPresent = 0; - int materialScannedChunks = 0; - int characteristicBlocks = 0; - int characteristicChunks = 0; - int vegetationBlocks = 0; - int vegetationColumns = 0; - int foundationBaseColumns = 0; - int foundationBlocks = 0; - int foundationColumns = 0; - int foundationGapColumns = 0; - BitSet visited = new BitSet(level.getHeight() << 8); - int[] maximumPieceY = new int[256]; - for (ChunkPos chunkPos : selected) { - ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z()); - StructureStart localStart = chunk.getStartForStructure(structure); - boolean validStart = localStart != null && localStart.isValid(); - int references = chunk.getReferencesForStructure(structure).size(); - if (hasNativeStructureEvidence(validStart, references)) { - evidenceChunks++; - } - BlockEntityAudit blockEntities = auditBlockEntities(level, chunk); - blockEntityStates += blockEntities.states(); - blockEntitiesPresent += blockEntities.present(); - StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check, - structureKey, visited, maximumPieceY); - if (material.scanned()) { - materialScannedChunks++; - } - characteristicBlocks += material.characteristicBlocks(); - if (material.characteristicBlocks() > 0) { - characteristicChunks++; - } - vegetationBlocks += material.vegetationBlocks(); - vegetationColumns += material.vegetationColumns(); - foundationBaseColumns += material.foundationBaseColumns(); - foundationBlocks += material.foundationBlocks(); - foundationColumns += material.foundationColumns(); - foundationGapColumns += material.foundationGapColumns(); - } - int coveredPieces = 0; - for (StructurePiece piece : pieces) { - boolean covered = false; - for (ChunkPos chunkPos : selected) { - if (intersectsChunk(piece.getBoundingBox(), chunkPos)) { - covered = true; - break; - } - } - if (covered) { - coveredPieces++; - } - } - return new FootprintAudit(selected.size(), availableChunks, evidenceChunks, - coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent, - materialScannedChunks, characteristicBlocks, characteristicChunks, - vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks, - foundationColumns, foundationGapColumns); - } - - private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk, - StructureStart start, - StructureCheck check, - Identifier structureKey, - BitSet visited, - int[] maximumPieceY) { - List pieces = start.getPieces(); - visited.clear(); - Arrays.fill(maximumPieceY, Integer.MIN_VALUE); - int characteristicBlocks = 0; - int minimumWorldY = level.getMinY(); - int maximumWorldY = level.getMaxY() - 1; - int minimumChunkX = chunk.getPos().getMinBlockX(); - int maximumChunkX = chunk.getPos().getMaxBlockX(); - int minimumChunkZ = chunk.getPos().getMinBlockZ(); - int maximumChunkZ = chunk.getPos().getMaxBlockZ(); - boolean scanned = false; - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - for (StructurePiece piece : pieces) { - BoundingBox bounds = piece.getBoundingBox(); - int minimumX = Math.max(minimumChunkX, bounds.minX()); - int maximumX = Math.min(maximumChunkX, bounds.maxX()); - int minimumY = Math.max(minimumWorldY, bounds.minY()); - int maximumY = Math.min(maximumWorldY, bounds.maxY()); - int minimumZ = Math.max(minimumChunkZ, bounds.minZ()); - int maximumZ = Math.min(maximumChunkZ, bounds.maxZ()); - if (minimumX > maximumX || minimumY > maximumY || minimumZ > maximumZ) { - continue; - } - scanned = true; - for (int z = minimumZ; z <= maximumZ; z++) { - int localZ = z - minimumChunkZ; - for (int x = minimumX; x <= maximumX; x++) { - int column = (localZ << 4) | (x - minimumChunkX); - maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY); - } - } - for (int y = minimumY; y <= maximumY; y++) { - int verticalIndex = (y - minimumWorldY) << 8; - for (int z = minimumZ; z <= maximumZ; z++) { - int localZ = z - minimumChunkZ; - for (int x = minimumX; x <= maximumX; x++) { - int column = (localZ << 4) | (x - minimumChunkX); - int index = verticalIndex | column; - if (visited.get(index)) { - continue; - } - visited.set(index); - BlockState state = chunk.getBlockState(position.set(x, y, z)); - Identifier blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock()); - if (isCharacteristicMaterial(check.label(), structureKey, blockKey)) { - characteristicBlocks++; - } - } - } - } - } - - int vegetationBlocks = 0; - int vegetationColumns = 0; - if (check.label().equals("mansion")) { - for (int column = 0; column < maximumPieceY.length; column++) { - int highestPieceY = maximumPieceY[column]; - if (highestPieceY == Integer.MIN_VALUE || highestPieceY >= maximumWorldY) { - continue; - } - boolean vegetationColumn = false; - int x = minimumChunkX + (column & 15); - int z = minimumChunkZ + (column >> 4); - for (int y = highestPieceY + 1; y <= maximumWorldY; y++) { - BlockState state = chunk.getBlockState(position.set(x, y, z)); - boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); - if (!mansionVegetationAbovePiece(vegetation, y, highestPieceY)) { - continue; - } - vegetationBlocks++; - vegetationColumn = true; - } - if (vegetationColumn) { - vegetationColumns++; - } - } - } - - int foundationBaseColumns = 0; - int foundationBlocks = 0; - int foundationColumns = 0; - int foundationGapColumns = 0; - if (check.label().equals("village")) { - BoundingBox area = new BoundingBox( - minimumChunkX, minimumWorldY, minimumChunkZ, - maximumChunkX, maximumWorldY, maximumChunkZ); - ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator(); - if (!(chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator)) { - throw new IllegalStateException("Iris structure audit requires the Iris chunk generator"); - } - Engine engine = irisGenerator.commandEngine(); - IntBinaryOperator surfaceHeight = (x, z) -> - engine.getHeight(x, z, true) + engine.getMinHeight(); - NativeStructurePostProcessor.StiltSupportAudit foundation = - NativeStructurePostProcessor.auditStiltSupport( - level, area, start, Blocks.COBBLESTONE.defaultBlockState(), surfaceHeight); - foundationBaseColumns = foundation.baseColumns(); - foundationBlocks = foundation.stiltBlocks(); - foundationColumns = foundation.stiltColumns(); - foundationGapColumns = foundation.unsupportedColumns(); - } - - return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks, - vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns, - foundationGapColumns); - } - - private static List selectFootprintChunks(StructureStart start, int limit) { - LinkedHashSet selected = new LinkedHashSet<>(); - addBounded(selected, start.getChunkPos(), limit); - List pieceAnchors = new ArrayList<>(); - for (StructurePiece piece : start.getPieces()) { - BoundingBox bounds = piece.getBoundingBox(); - pieceAnchors.add(new ChunkPos((bounds.minX() + bounds.maxX()) >> 5, - (bounds.minZ() + bounds.maxZ()) >> 5)); - pieceAnchors.add(new ChunkPos(bounds.minX() >> 4, bounds.minZ() >> 4)); - pieceAnchors.add(new ChunkPos(bounds.maxX() >> 4, bounds.maxZ() >> 4)); - } - pieceAnchors.sort(Comparator.comparingInt((ChunkPos chunkPos) -> - chunkPos.distanceSquared(start.getChunkPos()))); - for (ChunkPos chunkPos : pieceAnchors) { - addBounded(selected, chunkPos, limit); - } - for (ChunkPos chunkPos : boundedFootprintChunks(start.getBoundingBox(), start.getChunkPos(), limit)) { - if (intersectsAnyPiece(start.getPieces(), chunkPos)) { - addBounded(selected, chunkPos, limit); - } - } - return List.copyOf(selected); - } - - static List boundedFootprintChunks(BoundingBox bounds, ChunkPos origin, int limit) { - if (limit <= 0) { - return List.of(); - } - int minChunkX = bounds.minX() >> 4; - int maxChunkX = bounds.maxX() >> 4; - int minChunkZ = bounds.minZ() >> 4; - int maxChunkZ = bounds.maxZ() >> 4; - long width = (long) maxChunkX - minChunkX + 1L; - long depth = (long) maxChunkZ - minChunkZ + 1L; - long total = width * depth; - LinkedHashSet chunks = new LinkedHashSet<>(); - addBounded(chunks, origin, limit); - if (total <= limit) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit); - } - } - return List.copyOf(chunks); - } - addBounded(chunks, new ChunkPos(minChunkX, minChunkZ), limit); - addBounded(chunks, new ChunkPos(maxChunkX, minChunkZ), limit); - addBounded(chunks, new ChunkPos(minChunkX, maxChunkZ), limit); - addBounded(chunks, new ChunkPos(maxChunkX, maxChunkZ), limit); - int samplesPerAxis = Math.max(2, (int) Math.floor(Math.sqrt(limit))); - for (int sampleZ = 0; sampleZ < samplesPerAxis; sampleZ++) { - int chunkZ = sampleCoordinate(minChunkZ, maxChunkZ, sampleZ, samplesPerAxis); - for (int sampleX = 0; sampleX < samplesPerAxis; sampleX++) { - int chunkX = sampleCoordinate(minChunkX, maxChunkX, sampleX, samplesPerAxis); - addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit); - } - } - return List.copyOf(chunks); - } - - private static int footprintChunkCount(BoundingBox bounds) { - long width = (long) (bounds.maxX() >> 4) - (bounds.minX() >> 4) + 1L; - long depth = (long) (bounds.maxZ() >> 4) - (bounds.minZ() >> 4) + 1L; - long total = width * depth; - return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total; - } - - private static int sampleCoordinate(int minimum, int maximum, int index, int samples) { - if (samples <= 1 || minimum == maximum) { - return minimum; - } - double progress = (double) index / (double) (samples - 1); - return minimum + (int) Math.round((maximum - minimum) * progress); - } - - private static void addBounded(Set chunks, ChunkPos chunkPos, int limit) { - if (chunks.size() < limit) { - chunks.add(chunkPos); - } - } - - private static boolean intersectsAnyPiece(List pieces, ChunkPos chunkPos) { - for (StructurePiece piece : pieces) { - if (intersectsChunk(piece.getBoundingBox(), chunkPos)) { - return true; - } - } - return false; - } - - private static boolean intersectsChunk(BoundingBox bounds, ChunkPos chunkPos) { - return bounds.maxX() >= chunkPos.getMinBlockX() - && bounds.minX() <= chunkPos.getMaxBlockX() - && bounds.maxZ() >= chunkPos.getMinBlockZ() - && bounds.minZ() <= chunkPos.getMaxBlockZ(); - } - - private static BlockEntityAudit auditBlockEntities(ServerLevel level, ChunkAccess chunk) { - int states = 0; - int present = 0; - BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); - LevelChunkSection[] sections = chunk.getSections(); - for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { - LevelChunkSection section = sections[sectionIndex]; - if (!section.maybeHas(BlockState::hasBlockEntity)) { - continue; - } - int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; - for (int localY = 0; localY < 16; localY++) { - for (int localZ = 0; localZ < 16; localZ++) { - for (int localX = 0; localX < 16; localX++) { - BlockState state = section.getBlockState(localX, localY, localZ); - if (!state.hasBlockEntity()) { - continue; - } - states++; - position.set(chunk.getPos().getBlockX(localX), sectionMinY + localY, - chunk.getPos().getBlockZ(localZ)); - if (level.getBlockEntity(position) != null) { - present++; - } - } - } - } - } - return new BlockEntityAudit(states, present); - } - - private static PoiAudit auditStructurePois(ServerLevel level, StructureStart start) { - int inBounds = 0; - int outOfBounds = 0; - PoiManager poiManager = level.getPoiManager(); - for (ChunkPos chunkPos : selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS)) { - List records = poiManager.getInChunk( - holder -> true, chunkPos, PoiManager.Occupancy.ANY).toList(); - for (PoiRecord record : records) { - BlockPos position = record.getPos(); - if (position.getY() < level.getMinY() || position.getY() >= level.getMaxY()) { - outOfBounds++; - continue; - } - if (insideAnyPiece(start.getPieces(), position)) { - inBounds++; - } - } - } - return new PoiAudit(inBounds, outOfBounds); - } - - private static boolean insideAnyPiece(List pieces, BlockPos position) { - for (StructurePiece piece : pieces) { - if (piece.getBoundingBox().isInside(position)) { - return true; - } - } - return false; - } - - private static void emitSkipped(StructureCheck check, String reason, String... events) { - for (String event : events) { - qaEvent(event, check.label(), false, "skipped=" + reason); - } - } - - private static void qaEvent(String event, String structure, boolean pass, String detail) { - LOGGER.info(qaEventJson(event, structure, pass, detail)); - } - - static String qaEventJson(String event, String structure, boolean pass, String detail) { - return "QA_EVT {\"event\":\"" + jsonEscape(event) - + "\",\"structure\":\"" + jsonEscape(structure) - + "\",\"pass\":" + pass - + ",\"detail\":\"" + jsonEscape(detail) + "\"}"; - } - - static String jsonEscape(String value) { - StringBuilder escaped = new StringBuilder(value.length() + 16); - for (int i = 0; i < value.length(); i++) { - char character = value.charAt(i); - switch (character) { - case '"' -> escaped.append("\\\""); - case '\\' -> escaped.append("\\\\"); - case '\b' -> escaped.append("\\b"); - case '\f' -> escaped.append("\\f"); - case '\n' -> escaped.append("\\n"); - case '\r' -> escaped.append("\\r"); - case '\t' -> escaped.append("\\t"); - default -> { - if (character < 32) { - escaped.append("\\u"); - String hex = Integer.toHexString(character); - escaped.append("0".repeat(4 - hex.length())).append(hex); - } else { - escaped.append(character); - } - } - } - } - return escaped.toString(); - } - - static boolean hasNativeStructureEvidence(boolean validStart, int references) { - return validStart || references > 0; - } - - static boolean hasCharacteristicMaterialEvidence(int blocks, int chunksWithMaterial, int scannedChunks) { - if (blocks <= 0 || chunksWithMaterial <= 0 || scannedChunks <= 0 - || chunksWithMaterial > scannedChunks) { - return false; - } - return scannedChunks == 1 || chunksWithMaterial > 1; - } - - static boolean verticalShiftMatches(int configuredShift, Integer appliedShift, int shiftedMinY, - int shiftedMaxY, int worldMinY, int worldMaxYExclusive) { - try { - if (appliedShift == null) { - return configuredShift == 0 && StructureVerticalBounds.clampOffset( - shiftedMinY, shiftedMaxY, 0, worldMinY, worldMaxYExclusive) == 0; - } - int originalMinY = Math.subtractExact(shiftedMinY, appliedShift); - int originalMaxY = Math.subtractExact(shiftedMaxY, appliedShift); - int expectedShift = StructureVerticalBounds.clampOffset( - originalMinY, originalMaxY, configuredShift, worldMinY, worldMaxYExclusive); - return appliedShift == expectedShift; - } catch (RuntimeException error) { - return false; - } - } - - static boolean mansionVegetationPass(int remainingVegetationBlocks) { - return remainingVegetationBlocks == 0; - } - - static boolean mansionVegetationAbovePiece(boolean vegetation, int blockY, int highestPieceY) { - return vegetation && blockY > highestPieceY; - } - - static boolean villageFoundationPass(int unsupportedColumns) { - return unsupportedColumns == 0; - } - - static boolean villagePoiPass(int inBounds, int outOfBounds) { - return inBounds > 0 && outOfBounds == 0; - } - - static boolean isCharacteristicMaterial(String structureLabel, Identifier structureKey, Identifier blockKey) { - if (structureKey == null || blockKey == null || !blockKey.getNamespace().equals("minecraft")) { - return false; - } - String block = blockKey.getPath(); - return switch (structureLabel) { - case "stronghold" -> isStrongholdMaterial(block); - case "trial_chambers" -> isTrialChamberMaterial(block); - case "mansion" -> isWoodConstructionMaterial(block, "dark_oak") - || isWoodConstructionMaterial(block, "birch") - || isCobblestoneConstructionMaterial(block); - case "village" -> isVillageMaterial(structureKey.getPath(), block); - case "monument" -> isMonumentMaterial(block); - default -> false; - }; - } - - private static boolean isStrongholdMaterial(String block) { - return block.equals("stone_bricks") - || block.equals("cracked_stone_bricks") - || block.equals("mossy_stone_bricks") - || block.equals("infested_stone_bricks") - || block.equals("infested_cracked_stone_bricks") - || block.equals("infested_mossy_stone_bricks") - || block.equals("stone_brick_stairs") - || block.equals("stone_brick_slab") - || block.equals("stone_brick_wall"); - } - - private static boolean isTrialChamberMaterial(String block) { - return block.contains("tuff_brick") - || block.equals("polished_tuff") - || block.equals("chiseled_tuff") - || block.endsWith("copper_grate") - || block.equals("trial_spawner") - || block.equals("vault"); - } - - private static boolean isVillageMaterial(String structure, String block) { - if (isCobblestoneConstructionMaterial(block)) { - return true; - } - return switch (structure) { - case "village_plains" -> isWoodConstructionMaterial(block, "oak"); - case "village_desert" -> block.equals("cut_sandstone") - || block.equals("smooth_sandstone") - || block.equals("cut_sandstone_slab") - || block.equals("smooth_sandstone_slab") - || block.equals("smooth_sandstone_stairs") - || block.equals("sandstone_stairs") - || block.equals("sandstone_slab") - || block.equals("sandstone_wall"); - case "village_savanna" -> isWoodConstructionMaterial(block, "acacia"); - case "village_snowy", "village_taiga" -> isWoodConstructionMaterial(block, "spruce"); - default -> false; - }; - } - - private static boolean isWoodConstructionMaterial(String block, String wood) { - if (block.startsWith(wood)) { - int suffixOffset = wood.length(); - if (matchesSuffix(block, suffixOffset, "_planks") - || matchesSuffix(block, suffixOffset, "_stairs") - || matchesSuffix(block, suffixOffset, "_slab") - || matchesSuffix(block, suffixOffset, "_fence") - || matchesSuffix(block, suffixOffset, "_fence_gate") - || matchesSuffix(block, suffixOffset, "_door") - || matchesSuffix(block, suffixOffset, "_trapdoor")) { - return true; - } - } - int strippedOffset = "stripped_".length(); - if (!block.startsWith("stripped_") - || !block.regionMatches(strippedOffset, wood, 0, wood.length())) { - return false; - } - int suffixOffset = strippedOffset + wood.length(); - return matchesSuffix(block, suffixOffset, "_log") - || matchesSuffix(block, suffixOffset, "_wood"); - } - - private static boolean isCobblestoneConstructionMaterial(String block) { - return block.equals("cobblestone") - || block.equals("cobblestone_stairs") - || block.equals("cobblestone_slab") - || block.equals("cobblestone_wall") - || block.equals("mossy_cobblestone") - || block.equals("mossy_cobblestone_stairs") - || block.equals("mossy_cobblestone_slab") - || block.equals("mossy_cobblestone_wall"); - } - - private static boolean matchesSuffix(String value, int offset, String suffix) { - return value.length() == offset + suffix.length() - && value.regionMatches(offset, suffix, 0, suffix.length()); - } - - private static boolean isMonumentMaterial(String block) { - return block.equals("prismarine") - || block.equals("prismarine_bricks") - || block.equals("dark_prismarine") - || block.equals("sea_lantern"); - } - private static ServerLevel targetLevel(MinecraftServer server) { String target = System.getProperty("iris.worldcheck.dimension"); if (target != null && !target.isBlank()) { @@ -1266,61 +292,13 @@ public final class ModdedWorldCheck { return null; } - private static MessageDigest sha256() { - try { - return MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - @FunctionalInterface interface ProcessExit { void exit(int status); } - record DimensionContract(int minY, int height, int logicalHeight, double coordinateScale, - float ambientLight, boolean hasSkyLight, boolean hasCeiling, - boolean hasEnderDragonFight, int monsterSpawnBlockLightLimit) { - } - - private record StructureCheck(String label, List registryKeys, int locateRadius) { - } - private record WorldCheckPreparation(boolean nonStructurePass, boolean terrainOk, boolean dimensionTypeOk, boolean entityMixinsOk, NativeStructureGate structureGate) { } - - private record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed, - boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) { - } - - private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) { - } - - private record PendingVillagePoi(ServerLevel level, StructureStart start) { - } - - private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks, - int coveredPieces, int totalPieces, int blockEntityStates, - int blockEntitiesPresent, int materialScannedChunks, - int characteristicBlocks, int characteristicChunks, - int vegetationBlocks, int vegetationColumns, - int foundationBaseColumns, int foundationBlocks, int foundationColumns, - int foundationGapColumns) { - } - - private record BlockEntityAudit(int states, int present) { - } - - private record StructureMaterialAudit(boolean scanned, int characteristicBlocks, - int vegetationBlocks, int vegetationColumns, - int foundationBaseColumns, int foundationBlocks, - int foundationColumns, - int foundationGapColumns) { - } - - private record PoiAudit(int inBounds, int outOfBounds) { - } } 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 f59cb7da2..95218bc67 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 @@ -100,8 +100,8 @@ public final class ModdedWorldEngines { private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) { ModdedEngineBootstrap.bind(); - PackValidationRegistry.requireLoadable(pack); File packDir = resolvePack(pack, dimensionKey); + PackValidationRegistry.requireLoadable(pack); IrisData data = IrisData.openRuntime(packDir); IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); if (dimension == null) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java new file mode 100644 index 000000000..22af5f586 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckDimensionContract.java @@ -0,0 +1,114 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.volmlib.util.json.JSONObject; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.dimension.DimensionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class WorldCheckDimensionContract { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private WorldCheckDimensionContract() { + } + + static boolean checkDimensionType(ServerLevel level, IrisModdedChunkGenerator generator) { + try { + IrisDimension dimension = generator.commandEngine().getDimension(); + DimensionContract expected = expectedDimensionContract(dimension); + DimensionContract actual = runtimeDimensionContract(level.dimensionType()); + boolean pass = matchesDimensionContract(level.getMinY(), level.getHeight(), expected, actual); + String detail = "expected=" + expected + ",actual=" + actual + + ",levelMinY=" + level.getMinY() + ",levelHeight=" + level.getHeight(); + WorldCheckPredicates.qaEvent("dimension_type", dimension.getLoadKey(), pass, detail); + if (!pass) { + LOGGER.error("[worldcheck] dimension type mismatch for {}: {}", dimension.getLoadKey(), detail); + } else { + LOGGER.info("[worldcheck] dimension type contract: {}", detail); + } + return pass; + } catch (Throwable error) { + LOGGER.error("[worldcheck] could not validate the Iris dimension type contract", error); + WorldCheckPredicates.qaEvent("dimension_type", generator.activeDimensionKey(), false, + "validationError=" + error.getClass().getSimpleName() + ":" + error.getMessage()); + return false; + } + } + + static DimensionContract expectedDimensionContract(IrisDimension dimension) { + JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get())); + return new DimensionContract( + json.getInt("min_y"), + json.getInt("height"), + json.getInt("logical_height"), + json.getDouble("coordinate_scale"), + (float) json.getDouble("ambient_light"), + json.getBoolean("has_skylight"), + json.getBoolean("has_ceiling"), + json.getBoolean("has_ender_dragon_fight"), + json.getInt("monster_spawn_block_light_limit")); + } + + static DimensionContract runtimeDimensionContract(DimensionType dimensionType) { + return new DimensionContract( + dimensionType.minY(), + dimensionType.height(), + dimensionType.logicalHeight(), + dimensionType.coordinateScale(), + dimensionType.ambientLight(), + dimensionType.hasSkyLight(), + dimensionType.hasCeiling(), + dimensionType.hasEnderDragonFight(), + dimensionType.monsterSpawnBlockLightLimit()); + } + + static boolean matchesDimensionContract(int levelMinY, int levelHeight, + DimensionContract expected, DimensionContract actual) { + return levelMinY == expected.minY() + && levelHeight == expected.height() + && actual.equals(expected); + } + + static boolean checkEntityMixins(ServerLevel level) { + ItemEntity item = new ItemEntity(level, 0D, level.getMinY(), 0D, Items.COBBLESTONE.getDefaultInstance()); + boolean vanillaSave = item.shouldBeSaved(); + ModdedEntityPersistence.configure(item, false); + boolean suppressed = !item.shouldBeSaved(); + ModdedEntityPersistence.configure(item, true); + boolean restored = item.shouldBeSaved(); + boolean pass = vanillaSave && suppressed && restored; + WorldCheckPredicates.qaEvent("entity_mixin", "persistence", pass, + "vanilla=" + vanillaSave + ",suppressed=" + suppressed + ",restored=" + restored); + if (!pass) { + LOGGER.error("[worldcheck] shared entity mixins are not active on this loader"); + } + return pass; + } + + record DimensionContract(int minY, int height, int logicalHeight, double coordinateScale, + float ambientLight, boolean hasSkyLight, boolean hasCeiling, + boolean hasEnderDragonFight, int monsterSpawnBlockLightLimit) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java new file mode 100644 index 000000000..32fe1eccd --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckMaterials.java @@ -0,0 +1,130 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import net.minecraft.resources.Identifier; + +final class WorldCheckMaterials { + private WorldCheckMaterials() { + } + + static boolean isCharacteristicMaterial(String structureLabel, Identifier structureKey, Identifier blockKey) { + if (structureKey == null || blockKey == null || !blockKey.getNamespace().equals("minecraft")) { + return false; + } + String block = blockKey.getPath(); + return switch (structureLabel) { + case "stronghold" -> isStrongholdMaterial(block); + case "trial_chambers" -> isTrialChamberMaterial(block); + case "mansion" -> isWoodConstructionMaterial(block, "dark_oak") + || isWoodConstructionMaterial(block, "birch") + || isCobblestoneConstructionMaterial(block); + case "village" -> isVillageMaterial(structureKey.getPath(), block); + case "monument" -> isMonumentMaterial(block); + default -> false; + }; + } + + private static boolean isStrongholdMaterial(String block) { + return block.equals("stone_bricks") + || block.equals("cracked_stone_bricks") + || block.equals("mossy_stone_bricks") + || block.equals("infested_stone_bricks") + || block.equals("infested_cracked_stone_bricks") + || block.equals("infested_mossy_stone_bricks") + || block.equals("stone_brick_stairs") + || block.equals("stone_brick_slab") + || block.equals("stone_brick_wall"); + } + + private static boolean isTrialChamberMaterial(String block) { + return block.contains("tuff_brick") + || block.equals("polished_tuff") + || block.equals("chiseled_tuff") + || block.endsWith("copper_grate") + || block.equals("trial_spawner") + || block.equals("vault"); + } + + private static boolean isVillageMaterial(String structure, String block) { + if (isCobblestoneConstructionMaterial(block)) { + return true; + } + return switch (structure) { + case "village_plains" -> isWoodConstructionMaterial(block, "oak"); + case "village_desert" -> block.equals("cut_sandstone") + || block.equals("smooth_sandstone") + || block.equals("cut_sandstone_slab") + || block.equals("smooth_sandstone_slab") + || block.equals("smooth_sandstone_stairs") + || block.equals("sandstone_stairs") + || block.equals("sandstone_slab") + || block.equals("sandstone_wall"); + case "village_savanna" -> isWoodConstructionMaterial(block, "acacia"); + case "village_snowy", "village_taiga" -> isWoodConstructionMaterial(block, "spruce"); + default -> false; + }; + } + + private static boolean isWoodConstructionMaterial(String block, String wood) { + if (block.startsWith(wood)) { + int suffixOffset = wood.length(); + if (matchesSuffix(block, suffixOffset, "_planks") + || matchesSuffix(block, suffixOffset, "_stairs") + || matchesSuffix(block, suffixOffset, "_slab") + || matchesSuffix(block, suffixOffset, "_fence") + || matchesSuffix(block, suffixOffset, "_fence_gate") + || matchesSuffix(block, suffixOffset, "_door") + || matchesSuffix(block, suffixOffset, "_trapdoor")) { + return true; + } + } + int strippedOffset = "stripped_".length(); + if (!block.startsWith("stripped_") + || !block.regionMatches(strippedOffset, wood, 0, wood.length())) { + return false; + } + int suffixOffset = strippedOffset + wood.length(); + return matchesSuffix(block, suffixOffset, "_log") + || matchesSuffix(block, suffixOffset, "_wood"); + } + + private static boolean isCobblestoneConstructionMaterial(String block) { + return block.equals("cobblestone") + || block.equals("cobblestone_stairs") + || block.equals("cobblestone_slab") + || block.equals("cobblestone_wall") + || block.equals("mossy_cobblestone") + || block.equals("mossy_cobblestone_stairs") + || block.equals("mossy_cobblestone_slab") + || block.equals("mossy_cobblestone_wall"); + } + + private static boolean matchesSuffix(String value, int offset, String suffix) { + return value.length() == offset + suffix.length() + && value.regionMatches(offset, suffix, 0, suffix.length()); + } + + private static boolean isMonumentMaterial(String block) { + return block.equals("prismarine") + || block.equals("prismarine_bricks") + || block.equals("dark_prismarine") + || block.equals("sea_lantern"); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java new file mode 100644 index 000000000..12acad76b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckPredicates.java @@ -0,0 +1,130 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.StructureVerticalBounds; +import art.arcane.iris.modded.WorldCheckStructureAudit.StructureCheck; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +final class WorldCheckPredicates { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private WorldCheckPredicates() { + } + + static void emitSkipped(StructureCheck check, String reason, String... events) { + for (String event : events) { + qaEvent(event, check.label(), false, "skipped=" + reason); + } + } + + static void qaEvent(String event, String structure, boolean pass, String detail) { + LOGGER.info(qaEventJson(event, structure, pass, detail)); + } + + static String qaEventJson(String event, String structure, boolean pass, String detail) { + return "QA_EVT {\"event\":\"" + jsonEscape(event) + + "\",\"structure\":\"" + jsonEscape(structure) + + "\",\"pass\":" + pass + + ",\"detail\":\"" + jsonEscape(detail) + "\"}"; + } + + static String jsonEscape(String value) { + StringBuilder escaped = new StringBuilder(value.length() + 16); + for (int i = 0; i < value.length(); i++) { + char character = value.charAt(i); + switch (character) { + case '"' -> escaped.append("\\\""); + case '\\' -> escaped.append("\\\\"); + case '\b' -> escaped.append("\\b"); + case '\f' -> escaped.append("\\f"); + case '\n' -> escaped.append("\\n"); + case '\r' -> escaped.append("\\r"); + case '\t' -> escaped.append("\\t"); + default -> { + if (character < 32) { + escaped.append("\\u"); + String hex = Integer.toHexString(character); + escaped.append("0".repeat(4 - hex.length())).append(hex); + } else { + escaped.append(character); + } + } + } + } + return escaped.toString(); + } + + static boolean hasNativeStructureEvidence(boolean validStart, int references) { + return validStart || references > 0; + } + + static boolean hasCharacteristicMaterialEvidence(int blocks, int chunksWithMaterial, int scannedChunks) { + if (blocks <= 0 || chunksWithMaterial <= 0 || scannedChunks <= 0 + || chunksWithMaterial > scannedChunks) { + return false; + } + return scannedChunks == 1 || chunksWithMaterial > 1; + } + + static boolean verticalShiftMatches(int configuredShift, Integer appliedShift, int shiftedMinY, + int shiftedMaxY, int worldMinY, int worldMaxYExclusive) { + try { + if (appliedShift == null) { + return configuredShift == 0 && StructureVerticalBounds.clampOffset( + shiftedMinY, shiftedMaxY, 0, worldMinY, worldMaxYExclusive) == 0; + } + int originalMinY = Math.subtractExact(shiftedMinY, appliedShift); + int originalMaxY = Math.subtractExact(shiftedMaxY, appliedShift); + int expectedShift = StructureVerticalBounds.clampOffset( + originalMinY, originalMaxY, configuredShift, worldMinY, worldMaxYExclusive); + return appliedShift == expectedShift; + } catch (RuntimeException error) { + return false; + } + } + + static boolean mansionVegetationPass(int remainingVegetationBlocks) { + return remainingVegetationBlocks == 0; + } + + static boolean mansionVegetationAbovePiece(boolean vegetation, int blockY, int highestPieceY) { + return vegetation && blockY > highestPieceY; + } + + static boolean villageFoundationPass(int unsupportedColumns) { + return unsupportedColumns == 0; + } + + static boolean villagePoiPass(int inBounds, int outOfBounds) { + return inBounds > 0 && outOfBounds == 0; + } + + static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java new file mode 100644 index 000000000..dcd7630e5 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/WorldCheckStructureAudit.java @@ -0,0 +1,783 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.nativegen.NativeStructureFoundationBuilder; +import art.arcane.iris.nativegen.NativeStructureVegetationClearer; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.IntBinaryOperator; + +final class WorldCheckStructureAudit { + static final List STRUCTURE_CHECKS = List.of( + new StructureCheck("stronghold", List.of("minecraft:stronghold"), 256), + new StructureCheck("trial_chambers", List.of("minecraft:trial_chambers"), 128), + new StructureCheck("mansion", List.of("minecraft:mansion"), 256), + new StructureCheck("village", List.of( + "minecraft:village_plains", + "minecraft:village_desert", + "minecraft:village_savanna", + "minecraft:village_snowy", + "minecraft:village_taiga"), 128), + new StructureCheck("monument", List.of("minecraft:monument"), 128) + ); + private static final int MAX_FOOTPRINT_CHUNKS = 96; + private static final int MAX_START_REFERENCE_CHUNKS = 16; + private static final int MAX_STRUCTURE_CANDIDATES = 1024; + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private WorldCheckStructureAudit() { + } + + static NativeStructureGate checkNativeStructures(ServerLevel level, + IrisModdedChunkGenerator generator, + BlockPos origin) { + boolean pass = true; + int nonVillagePassed = 0; + boolean villagePass = false; + PendingVillagePoi pendingPoi = null; + for (StructureCheck check : STRUCTURE_CHECKS) { + StructureCheckResult result = checkNativeStructure(level, generator, origin, check); + if (!result.pass()) { + pass = false; + } + if (check.label().equals("village")) { + villagePass = result.pass(); + pendingPoi = result.pendingPoi(); + } else if (result.pass()) { + nonVillagePassed++; + } + } + return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi); + } + + private static StructureCheckResult checkNativeStructure(ServerLevel level, + IrisModdedChunkGenerator generator, + BlockPos origin, + StructureCheck check) { + Registry registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + List> registered = new ArrayList<>(check.registryKeys().size()); + LinkedHashSet registeredKeys = new LinkedHashSet<>(); + for (String key : check.registryKeys()) { + Identifier identifier = Identifier.tryParse(key); + if (identifier == null) { + continue; + } + Optional> resolved = registry.get(identifier); + if (resolved.isPresent()) { + registered.add(resolved.get()); + registeredKeys.add(identifier.toString()); + } + } + boolean registryOk = registered.size() == check.registryKeys().size(); + LOGGER.info("[worldcheck] {} registry: {}/{} resolved {}", check.label(), registered.size(), + check.registryKeys().size(), registeredKeys); + WorldCheckPredicates.qaEvent("structure_registry", check.label(), registryOk, + "resolved=" + registered.size() + ",expected=" + check.registryKeys().size() + + ",keys=" + String.join("|", registeredKeys)); + if (!registryOk) { + LOGGER.error("[worldcheck] {} registry resolution failed; expected {}", check.label(), check.registryKeys()); + WorldCheckPredicates.emitSkipped(check, "registry", "structure_reachability", "structure_locate", + "structure_start_reference", "structure_footprint", "structure_material", + "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + List> reachable = new ArrayList<>(registered.size()); + LinkedHashSet reachableKeys = new LinkedHashSet<>(); + for (Holder holder : registered) { + if (!generator.isNativeStructureReachable(holder)) { + continue; + } + reachable.add(holder); + Identifier key = registry.getKey(holder.value()); + if (key != null) { + reachableKeys.add(key.toString()); + } + } + boolean reachableOk = !reachable.isEmpty(); + LOGGER.info("[worldcheck] {} biome-reachable through Iris: {}", check.label(), reachableKeys); + WorldCheckPredicates.qaEvent("structure_reachability", check.label(), reachableOk, + "reachable=" + reachable.size() + ",registered=" + registered.size() + + ",keys=" + String.join("|", reachableKeys)); + if (!reachableOk) { + LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label()); + WorldCheckPredicates.emitSkipped(check, "reachability", "structure_locate", "structure_start_reference", + "structure_footprint", "structure_material", "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + long locateStart = System.nanoTime(); + Pair> found = findGeneratedStructureCandidate( + level, reachable, origin, check.locateRadius()); + long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L; + Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value()); + boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString()); + WorldCheckPredicates.qaEvent("structure_locate", check.label(), locateOk, + "method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius() + + ",result=" + (foundKey == null ? "none" : foundKey)); + if (found == null) { + LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms", + check.label(), check.locateRadius(), locateMillis); + WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint", + "structure_material", "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + BlockPos position = found.getFirst(); + LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})", + check.label(), position.getX(), position.getY(), position.getZ(), locateMillis, + check.locateRadius(), foundKey); + if (!locateOk) { + LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey); + WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint", + "structure_material", "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + int chunkX = position.getX() >> 4; + int chunkZ = position.getZ() >> 4; + ChunkAccess targetChunk = level.getChunk(chunkX, chunkZ); + Structure structure = found.getSecond().value(); + StructureStart start = resolveStructureStart(level, targetChunk, structure); + boolean validStart = start != null && start.isValid(); + int references = targetChunk.getReferencesForStructure(structure).size(); + boolean startReferenceOk = WorldCheckPredicates.hasNativeStructureEvidence(validStart, references); + LOGGER.info("[worldcheck] {} target chunk {},{}: valid start={}, references={}", + check.label(), chunkX, chunkZ, validStart, references); + WorldCheckPredicates.qaEvent("structure_start_reference", check.label(), startReferenceOk, + "chunk=" + chunkX + "," + chunkZ + ",validStart=" + validStart + + ",references=" + references); + if (!startReferenceOk || !validStart) { + LOGGER.error("[worldcheck] {} located at chunk {},{} but no resolvable valid start was generated", + check.label(), chunkX, chunkZ); + WorldCheckPredicates.emitSkipped(check, "start_reference", "structure_footprint", "structure_material", + "structure_block_entity"); + return new StructureCheckResult(false, null); + } + + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve( + generator.commandEngine(), foundKey.toString(), + NativeStructureVegetationClearer.isUndergroundStep(structure.step())); + Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos()); + BoundingBox shiftedBounds = start.getBoundingBox(); + boolean verticalShiftOk = WorldCheckPredicates.verticalShiftMatches( + decision.yShift(), appliedShift, shiftedBounds.minY(), shiftedBounds.maxY(), + level.getMinY(), level.getMaxY()); + WorldCheckPredicates.qaEvent("structure_vertical_shift", check.label(), verticalShiftOk, + "configured=" + decision.yShift() + ",applied=" + + (appliedShift == null ? "unrecorded" : appliedShift)); + if (!verticalShiftOk) { + LOGGER.error("[worldcheck] {} expected vertical shift {} but generation recorded {}", + check.label(), decision.yShift(), appliedShift); + } + + FootprintAudit footprint = auditFootprint(level, structure, start, check, foundKey); + boolean footprintOk = footprint.inspectedChunks() > 0 + && footprint.evidenceChunks() == footprint.inspectedChunks() + && footprint.coveredPieces() == footprint.totalPieces(); + LOGGER.info("[worldcheck] {} footprint: chunks={}/{} evidence={} pieces={}/{}", + check.label(), footprint.inspectedChunks(), footprint.availableChunks(), + footprint.evidenceChunks(), footprint.coveredPieces(), footprint.totalPieces()); + WorldCheckPredicates.qaEvent("structure_footprint", check.label(), footprintOk, + "inspected=" + footprint.inspectedChunks() + ",available=" + footprint.availableChunks() + + ",evidence=" + footprint.evidenceChunks() + ",coveredPieces=" + + footprint.coveredPieces() + ",totalPieces=" + footprint.totalPieces()); + + boolean materialOk = WorldCheckPredicates.hasCharacteristicMaterialEvidence(footprint.characteristicBlocks(), + footprint.characteristicChunks(), footprint.materialScannedChunks()); + LOGGER.info("[worldcheck] {} material: blocks={} chunks={}/{}", + check.label(), footprint.characteristicBlocks(), footprint.characteristicChunks(), + footprint.materialScannedChunks()); + WorldCheckPredicates.qaEvent("structure_material", check.label(), materialOk, + "blocks=" + footprint.characteristicBlocks() + ",chunks=" + footprint.characteristicChunks() + + ",scanned=" + footprint.materialScannedChunks()); + + boolean vegetationOk = true; + if (check.label().equals("mansion")) { + boolean overlap = footprint.vegetationBlocks() > 0; + vegetationOk = WorldCheckPredicates.mansionVegetationPass(footprint.vegetationBlocks()); + LOGGER.info("[worldcheck] mansion vegetation metric: remaining log/leaf blocks={} columns={} overlap={}", + footprint.vegetationBlocks(), footprint.vegetationColumns(), overlap); + WorldCheckPredicates.qaEvent("mansion_vegetation_metric", check.label(), vegetationOk, + "remainingLogsOrLeaves=" + footprint.vegetationBlocks() + ",columns=" + + footprint.vegetationColumns() + ",overlap=" + overlap); + } + + boolean foundationOk = true; + PendingVillagePoi pendingPoi = null; + if (check.label().equals("village")) { + foundationOk = WorldCheckPredicates.villageFoundationPass(footprint.foundationGapColumns()); + LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}", + footprint.foundationBaseColumns(), footprint.foundationBlocks(), + footprint.foundationColumns(), footprint.foundationGapColumns()); + WorldCheckPredicates.qaEvent("village_foundation_metric", check.label(), foundationOk, + "bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase=" + + footprint.foundationBlocks() + ",columns=" + + footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns()); + pendingPoi = new PendingVillagePoi(level, start); + } + + boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent(); + LOGGER.info("[worldcheck] {} block entities: state blocks={}, present={}, missing={}", + check.label(), footprint.blockEntityStates(), footprint.blockEntitiesPresent(), + footprint.blockEntityStates() - footprint.blockEntitiesPresent()); + WorldCheckPredicates.qaEvent("structure_block_entity", check.label(), blockEntityOk, + "states=" + footprint.blockEntityStates() + ",present=" + footprint.blockEntitiesPresent() + + ",missing=" + (footprint.blockEntityStates() - footprint.blockEntitiesPresent())); + if (!footprintOk) { + LOGGER.error("[worldcheck] {} structure footprint is incomplete", check.label()); + } + if (!materialOk) { + LOGGER.error("[worldcheck] {} has no distributed characteristic structure material", check.label()); + } + if (!blockEntityOk) { + LOGGER.error("[worldcheck] {} generated block-entity states without matching block entities", check.label()); + } + if (!vegetationOk) { + LOGGER.error("[worldcheck] mansion vegetation still intersects the generated structure footprint"); + } + if (!foundationOk) { + LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement"); + } + boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk + && vegetationOk && foundationOk; + return new StructureCheckResult(pass, pass ? pendingPoi : null); + } + + private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk, + Structure structure) { + StructureStart direct = targetChunk.getStartForStructure(structure); + if (direct != null && direct.isValid()) { + return direct; + } + int checked = 0; + for (long packed : targetChunk.getReferencesForStructure(structure)) { + if (checked++ >= MAX_START_REFERENCE_CHUNKS) { + break; + } + ChunkAccess referencedChunk = level.getChunk(ChunkPos.getX(packed), ChunkPos.getZ(packed)); + StructureStart referenced = referencedChunk.getStartForStructure(structure); + if (referenced != null && referenced.isValid()) { + return referenced; + } + } + return direct; + } + + private static Pair> findGeneratedStructureCandidate( + ServerLevel level, List> structures, BlockPos origin, int maxRadius) { + ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState(); + Set attempted = new LinkedHashSet<>(); + for (Holder structure : structures) { + for (StructurePlacement placement : state.getPlacementsForStructure(structure)) { + if (!(placement instanceof ConcentricRingsStructurePlacement rings)) { + continue; + } + List positions = state.getRingPositionsFor(rings); + if (positions == null) { + continue; + } + List sorted = new ArrayList<>(positions); + sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position))); + for (ChunkPos position : sorted) { + Pair> found = inspectStructureCandidate( + level, structures, placement, position, attempted); + if (found != null) { + return found; + } + if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) { + return null; + } + } + } + } + + int originChunkX = origin.getX() >> 4; + int originChunkZ = origin.getZ() >> 4; + for (int radius = 0; radius <= maxRadius; radius++) { + for (Holder structure : structures) { + for (StructurePlacement placement : state.getPlacementsForStructure(structure)) { + if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) { + continue; + } + for (int x = -radius; x <= radius; x++) { + boolean xEdge = x == -radius || x == radius; + for (int z = -radius; z <= radius; z++) { + if (!xEdge && z != -radius && z != radius) { + continue; + } + int sectorX = originChunkX + randomSpread.spacing() * x; + int sectorZ = originChunkZ + randomSpread.spacing() * z; + ChunkPos candidate = randomSpread.getPotentialStructureChunk( + state.getLevelSeed(), sectorX, sectorZ); + if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) { + continue; + } + Pair> found = inspectStructureCandidate( + level, structures, placement, candidate, attempted); + if (found != null) { + return found; + } + if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) { + return null; + } + } + } + } + } + } + return null; + } + + private static Pair> inspectStructureCandidate( + ServerLevel level, List> structures, StructurePlacement placement, + ChunkPos candidate, Set attempted) { + if (!attempted.add(candidate.pack())) { + return null; + } + ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z()); + for (Holder structure : structures) { + StructureStart start = resolveStructureStart(level, chunk, structure.value()); + if (start == null || !start.isValid()) { + continue; + } + BlockPos locate = placement.getLocatePos(start.getChunkPos()); + BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ()); + return Pair.of(resolved, structure); + } + return null; + } + + private static long distanceSquared(BlockPos origin, ChunkPos position) { + long x = (long) position.getMinBlockX() - origin.getX(); + long z = (long) position.getMinBlockZ() - origin.getZ(); + return x * x + z * z; + } + + private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start, + StructureCheck check, Identifier structureKey) { + List pieces = start.getPieces(); + BoundingBox bounds = start.getBoundingBox(); + int availableChunks = footprintChunkCount(bounds); + List selected = selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS); + int evidenceChunks = 0; + int blockEntityStates = 0; + int blockEntitiesPresent = 0; + int materialScannedChunks = 0; + int characteristicBlocks = 0; + int characteristicChunks = 0; + int vegetationBlocks = 0; + int vegetationColumns = 0; + int foundationBaseColumns = 0; + int foundationBlocks = 0; + int foundationColumns = 0; + int foundationGapColumns = 0; + BitSet visited = new BitSet(level.getHeight() << 8); + int[] maximumPieceY = new int[256]; + for (ChunkPos chunkPos : selected) { + ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z()); + StructureStart localStart = chunk.getStartForStructure(structure); + boolean validStart = localStart != null && localStart.isValid(); + int references = chunk.getReferencesForStructure(structure).size(); + if (WorldCheckPredicates.hasNativeStructureEvidence(validStart, references)) { + evidenceChunks++; + } + BlockEntityAudit blockEntities = auditBlockEntities(level, chunk); + blockEntityStates += blockEntities.states(); + blockEntitiesPresent += blockEntities.present(); + StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check, + structureKey, visited, maximumPieceY); + if (material.scanned()) { + materialScannedChunks++; + } + characteristicBlocks += material.characteristicBlocks(); + if (material.characteristicBlocks() > 0) { + characteristicChunks++; + } + vegetationBlocks += material.vegetationBlocks(); + vegetationColumns += material.vegetationColumns(); + foundationBaseColumns += material.foundationBaseColumns(); + foundationBlocks += material.foundationBlocks(); + foundationColumns += material.foundationColumns(); + foundationGapColumns += material.foundationGapColumns(); + } + int coveredPieces = 0; + for (StructurePiece piece : pieces) { + boolean covered = false; + for (ChunkPos chunkPos : selected) { + if (intersectsChunk(piece.getBoundingBox(), chunkPos)) { + covered = true; + break; + } + } + if (covered) { + coveredPieces++; + } + } + return new FootprintAudit(selected.size(), availableChunks, evidenceChunks, + coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent, + materialScannedChunks, characteristicBlocks, characteristicChunks, + vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks, + foundationColumns, foundationGapColumns); + } + + private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk, + StructureStart start, + StructureCheck check, + Identifier structureKey, + BitSet visited, + int[] maximumPieceY) { + List pieces = start.getPieces(); + visited.clear(); + Arrays.fill(maximumPieceY, Integer.MIN_VALUE); + int characteristicBlocks = 0; + int minimumWorldY = level.getMinY(); + int maximumWorldY = level.getMaxY() - 1; + int minimumChunkX = chunk.getPos().getMinBlockX(); + int maximumChunkX = chunk.getPos().getMaxBlockX(); + int minimumChunkZ = chunk.getPos().getMinBlockZ(); + int maximumChunkZ = chunk.getPos().getMaxBlockZ(); + boolean scanned = false; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (StructurePiece piece : pieces) { + BoundingBox bounds = piece.getBoundingBox(); + int minimumX = Math.max(minimumChunkX, bounds.minX()); + int maximumX = Math.min(maximumChunkX, bounds.maxX()); + int minimumY = Math.max(minimumWorldY, bounds.minY()); + int maximumY = Math.min(maximumWorldY, bounds.maxY()); + int minimumZ = Math.max(minimumChunkZ, bounds.minZ()); + int maximumZ = Math.min(maximumChunkZ, bounds.maxZ()); + if (minimumX > maximumX || minimumY > maximumY || minimumZ > maximumZ) { + continue; + } + scanned = true; + for (int z = minimumZ; z <= maximumZ; z++) { + int localZ = z - minimumChunkZ; + for (int x = minimumX; x <= maximumX; x++) { + int column = (localZ << 4) | (x - minimumChunkX); + maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY); + } + } + for (int y = minimumY; y <= maximumY; y++) { + int verticalIndex = (y - minimumWorldY) << 8; + for (int z = minimumZ; z <= maximumZ; z++) { + int localZ = z - minimumChunkZ; + for (int x = minimumX; x <= maximumX; x++) { + int column = (localZ << 4) | (x - minimumChunkX); + int index = verticalIndex | column; + if (visited.get(index)) { + continue; + } + visited.set(index); + BlockState state = chunk.getBlockState(position.set(x, y, z)); + Identifier blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock()); + if (WorldCheckMaterials.isCharacteristicMaterial(check.label(), structureKey, blockKey)) { + characteristicBlocks++; + } + } + } + } + } + + int vegetationBlocks = 0; + int vegetationColumns = 0; + if (check.label().equals("mansion")) { + for (int column = 0; column < maximumPieceY.length; column++) { + int highestPieceY = maximumPieceY[column]; + if (highestPieceY == Integer.MIN_VALUE || highestPieceY >= maximumWorldY) { + continue; + } + boolean vegetationColumn = false; + int x = minimumChunkX + (column & 15); + int z = minimumChunkZ + (column >> 4); + for (int y = highestPieceY + 1; y <= maximumWorldY; y++) { + BlockState state = chunk.getBlockState(position.set(x, y, z)); + boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + if (!WorldCheckPredicates.mansionVegetationAbovePiece(vegetation, y, highestPieceY)) { + continue; + } + vegetationBlocks++; + vegetationColumn = true; + } + if (vegetationColumn) { + vegetationColumns++; + } + } + } + + int foundationBaseColumns = 0; + int foundationBlocks = 0; + int foundationColumns = 0; + int foundationGapColumns = 0; + if (check.label().equals("village")) { + BoundingBox area = new BoundingBox( + minimumChunkX, minimumWorldY, minimumChunkZ, + maximumChunkX, maximumWorldY, maximumChunkZ); + ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator(); + if (!(chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator)) { + throw new IllegalStateException("Iris structure audit requires the Iris chunk generator"); + } + Engine engine = irisGenerator.commandEngine(); + IntBinaryOperator surfaceHeight = (x, z) -> + engine.getHeight(x, z, true) + engine.getMinHeight(); + NativeStructureFoundationBuilder.StiltSupportAudit foundation = + NativeStructureFoundationBuilder.auditStiltSupport( + level, area, start, Blocks.COBBLESTONE.defaultBlockState(), surfaceHeight); + foundationBaseColumns = foundation.baseColumns(); + foundationBlocks = foundation.stiltBlocks(); + foundationColumns = foundation.stiltColumns(); + foundationGapColumns = foundation.unsupportedColumns(); + } + + return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks, + vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns, + foundationGapColumns); + } + + private static List selectFootprintChunks(StructureStart start, int limit) { + LinkedHashSet selected = new LinkedHashSet<>(); + addBounded(selected, start.getChunkPos(), limit); + List pieceAnchors = new ArrayList<>(); + for (StructurePiece piece : start.getPieces()) { + BoundingBox bounds = piece.getBoundingBox(); + pieceAnchors.add(new ChunkPos((bounds.minX() + bounds.maxX()) >> 5, + (bounds.minZ() + bounds.maxZ()) >> 5)); + pieceAnchors.add(new ChunkPos(bounds.minX() >> 4, bounds.minZ() >> 4)); + pieceAnchors.add(new ChunkPos(bounds.maxX() >> 4, bounds.maxZ() >> 4)); + } + pieceAnchors.sort(Comparator.comparingInt((ChunkPos chunkPos) -> + chunkPos.distanceSquared(start.getChunkPos()))); + for (ChunkPos chunkPos : pieceAnchors) { + addBounded(selected, chunkPos, limit); + } + for (ChunkPos chunkPos : boundedFootprintChunks(start.getBoundingBox(), start.getChunkPos(), limit)) { + if (intersectsAnyPiece(start.getPieces(), chunkPos)) { + addBounded(selected, chunkPos, limit); + } + } + return List.copyOf(selected); + } + + static List boundedFootprintChunks(BoundingBox bounds, ChunkPos origin, int limit) { + if (limit <= 0) { + return List.of(); + } + int minChunkX = bounds.minX() >> 4; + int maxChunkX = bounds.maxX() >> 4; + int minChunkZ = bounds.minZ() >> 4; + int maxChunkZ = bounds.maxZ() >> 4; + long width = (long) maxChunkX - minChunkX + 1L; + long depth = (long) maxChunkZ - minChunkZ + 1L; + long total = width * depth; + LinkedHashSet chunks = new LinkedHashSet<>(); + addBounded(chunks, origin, limit); + if (total <= limit) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit); + } + } + return List.copyOf(chunks); + } + addBounded(chunks, new ChunkPos(minChunkX, minChunkZ), limit); + addBounded(chunks, new ChunkPos(maxChunkX, minChunkZ), limit); + addBounded(chunks, new ChunkPos(minChunkX, maxChunkZ), limit); + addBounded(chunks, new ChunkPos(maxChunkX, maxChunkZ), limit); + int samplesPerAxis = Math.max(2, (int) Math.floor(Math.sqrt(limit))); + for (int sampleZ = 0; sampleZ < samplesPerAxis; sampleZ++) { + int chunkZ = sampleCoordinate(minChunkZ, maxChunkZ, sampleZ, samplesPerAxis); + for (int sampleX = 0; sampleX < samplesPerAxis; sampleX++) { + int chunkX = sampleCoordinate(minChunkX, maxChunkX, sampleX, samplesPerAxis); + addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit); + } + } + return List.copyOf(chunks); + } + + private static int footprintChunkCount(BoundingBox bounds) { + long width = (long) (bounds.maxX() >> 4) - (bounds.minX() >> 4) + 1L; + long depth = (long) (bounds.maxZ() >> 4) - (bounds.minZ() >> 4) + 1L; + long total = width * depth; + return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total; + } + + private static int sampleCoordinate(int minimum, int maximum, int index, int samples) { + if (samples <= 1 || minimum == maximum) { + return minimum; + } + double progress = (double) index / (double) (samples - 1); + return minimum + (int) Math.round((maximum - minimum) * progress); + } + + private static void addBounded(Set chunks, ChunkPos chunkPos, int limit) { + if (chunks.size() < limit) { + chunks.add(chunkPos); + } + } + + private static boolean intersectsAnyPiece(List pieces, ChunkPos chunkPos) { + for (StructurePiece piece : pieces) { + if (intersectsChunk(piece.getBoundingBox(), chunkPos)) { + return true; + } + } + return false; + } + + private static boolean intersectsChunk(BoundingBox bounds, ChunkPos chunkPos) { + return bounds.maxX() >= chunkPos.getMinBlockX() + && bounds.minX() <= chunkPos.getMaxBlockX() + && bounds.maxZ() >= chunkPos.getMinBlockZ() + && bounds.minZ() <= chunkPos.getMaxBlockZ(); + } + + private static BlockEntityAudit auditBlockEntities(ServerLevel level, ChunkAccess chunk) { + int states = 0; + int present = 0; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + LevelChunkSection[] sections = chunk.getSections(); + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + LevelChunkSection section = sections[sectionIndex]; + if (!section.maybeHas(BlockState::hasBlockEntity)) { + continue; + } + int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4; + for (int localY = 0; localY < 16; localY++) { + for (int localZ = 0; localZ < 16; localZ++) { + for (int localX = 0; localX < 16; localX++) { + BlockState state = section.getBlockState(localX, localY, localZ); + if (!state.hasBlockEntity()) { + continue; + } + states++; + position.set(chunk.getPos().getBlockX(localX), sectionMinY + localY, + chunk.getPos().getBlockZ(localZ)); + if (level.getBlockEntity(position) != null) { + present++; + } + } + } + } + } + return new BlockEntityAudit(states, present); + } + + static PoiAudit auditStructurePois(ServerLevel level, StructureStart start) { + int inBounds = 0; + int outOfBounds = 0; + PoiManager poiManager = level.getPoiManager(); + for (ChunkPos chunkPos : selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS)) { + List records = poiManager.getInChunk( + holder -> true, chunkPos, PoiManager.Occupancy.ANY).toList(); + for (PoiRecord record : records) { + BlockPos position = record.getPos(); + if (position.getY() < level.getMinY() || position.getY() >= level.getMaxY()) { + outOfBounds++; + continue; + } + if (insideAnyPiece(start.getPieces(), position)) { + inBounds++; + } + } + } + return new PoiAudit(inBounds, outOfBounds); + } + + private static boolean insideAnyPiece(List pieces, BlockPos position) { + for (StructurePiece piece : pieces) { + if (piece.getBoundingBox().isInside(position)) { + return true; + } + } + return false; + } + + record StructureCheck(String label, List registryKeys, int locateRadius) { + } + + record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed, + boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) { + } + + private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) { + } + + record PendingVillagePoi(ServerLevel level, StructureStart start) { + } + + private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks, + int coveredPieces, int totalPieces, int blockEntityStates, + int blockEntitiesPresent, int materialScannedChunks, + int characteristicBlocks, int characteristicChunks, + int vegetationBlocks, int vegetationColumns, + int foundationBaseColumns, int foundationBlocks, int foundationColumns, + int foundationGapColumns) { + } + + private record BlockEntityAudit(int states, int present) { + } + + private record StructureMaterialAudit(boolean scanned, int characteristicBlocks, + int vegetationBlocks, int vegetationColumns, + int foundationBaseColumns, int foundationBlocks, + int foundationColumns, + int foundationGapColumns) { + } + + record PoiAudit(int inBounds, int outOfBounds) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java index 2cb7d7ca2..88cd88f35 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/IrisModdedAPI.java @@ -25,10 +25,33 @@ import art.arcane.iris.modded.command.ModdedPregenJob; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.chunk.ChunkGenerator; +/** + * Entry point for mods integrating with Iris on Fabric, Forge and NeoForge. + *

+ * Everything here is static and null-tolerant: a null or non-Iris {@link ServerLevel} produces false, null, or a + * no-op rather than an exception, so a caller never has to pre-check whether a level is generated by Iris. + *

+ * Threading. {@link #isIrisLevel(ServerLevel)}, {@link #isStudioLevel(ServerLevel)} and + * {@link #getEngine(ServerLevel)} read the level's chunk generator reference and are safe from any thread once + * the level is loaded. The mantle accessors are safe off the server thread but touch engine storage - see their + * own notes. {@link #pregenerate(ServerLevel, int)} and {@link #registerProvider(ModdedDataProvider)} mutate + * global state and belong on the server thread, during mod setup or from a command. + *

+ * Stability. This class and the {@code Modded*} types beside it are the intended integration surface. The + * types they expose from {@code art.arcane.iris.engine.*} and {@code art.arcane.iris.core.*} - notably + * {@link Engine} - are internal to Iris and change without a deprecation cycle. Treat {@link Engine} as an opaque + * token to hand back to Iris, and prefer the wrappers here over reaching into it. + * + * @see ModdedDataProvider for supplying custom blocks, items and entities to the generator + */ public final class IrisModdedAPI { private IrisModdedAPI() { } + /** + * Whether {@code level}'s chunk generator is an Iris generator. False for null and for every vanilla or + * third-party generated level. The cheapest available Iris check. + */ public static boolean isIrisLevel(ServerLevel level) { if (level == null) { return false; @@ -36,11 +59,22 @@ public final class IrisModdedAPI { return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator; } + /** + * Whether {@code level} is an Iris studio level - a throwaway world opened for pack authoring, which is + * deleted on shutdown. Persist nothing against one. False for null and for non-Iris levels. + */ public static boolean isStudioLevel(ServerLevel level) { Engine engine = getEngine(level); return engine != null && engine.isStudio(); } + /** + * The Iris engine driving {@code level}, or null when the level is null, is not Iris-generated, or its engine + * is not currently available - during shutdown, or while the generator is still binding. + *

+ * Never cached: resolve per use. Reloading a pack or unloading the level replaces the engine, and a stale + * reference goes inert. {@link Engine} is internal to Iris; see the stability note on this class. + */ public static Engine getEngine(ServerLevel level) { if (level == null) { return null; @@ -56,10 +90,27 @@ public final class IrisModdedAPI { } } + /** + * Starts a cached, asynchronous pregeneration of {@code radiusBlocks} around the world origin. + * Equivalent to {@code pregenerate(level, radiusBlocks, 0, 0, false, true)}. + */ public static boolean pregenerate(ServerLevel level, int radiusBlocks) { return pregenerate(level, radiusBlocks, 0, 0, false, true); } + /** + * Starts a pregeneration job over a square region. + *

+ * Returns as soon as the job is queued; progress is reported through Iris's own logging and boss bar, not to + * the caller. Only one job runs server-wide, so this returns false if one is already active. Call on the + * server thread. + * + * @param radiusBlocks half-extent of the square in blocks, measured from the centre + * @param sync write chunks synchronously; slower but avoids the async write queue + * @param cached reuse and update the on-disk pregeneration cache so an interrupted job resumes instead of + * regenerating + * @return false when {@code level} is not Iris-generated or another pregeneration job is already running + */ public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) { Engine engine = getEngine(level); if (engine == null) { @@ -68,6 +119,19 @@ public final class IrisModdedAPI { return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached); } + /** + * Reads a mantle value of {@code type} at world coordinates. + *

+ * The mantle is Iris's own per-block sidecar storage, independent of chunk NBT, and it is how Iris carries + * data that must survive between generation stages. Coordinates are world-space: {@code y} is translated by + * the engine's minimum height internally. + *

+ * Returns null when the level is not Iris-generated, no mantle region exists for that column yet - reads + * never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's + * height range reads as null rather than throwing. + * + * @throws IllegalStateException if the engine's mantle has already been closed + */ public static T getMantleData(ServerLevel level, int x, int y, int z, Class type) { Engine engine = getEngine(level); if (engine == null) { @@ -76,6 +140,19 @@ public final class IrisModdedAPI { return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type); } + /** + * Writes a mantle value at world coordinates, replacing any previous value of the same type there. + *

+ * Unlike {@link #getMantleData(ServerLevel, int, int, int, Class)}, a write creates the mantle region if it + * does not exist, which can touch disk - do not call it per block in a tick loop from the server thread. A + * null {@code data}, a non-Iris level, or a {@code y} outside the engine's height range is a silent no-op; + * remove values with {@link #deleteMantleData(ServerLevel, int, int, int, Class)}. + *

+ * Values written under a custom type are discarded when Iris trims a mantle region unless the type is + * declared with {@link #retainMantleDataForSlice(Class)}. + * + * @throws IllegalStateException if the engine's mantle has already been closed + */ public static void setMantleData(ServerLevel level, int x, int y, int z, T data) { Engine engine = getEngine(level); if (engine == null || data == null) { @@ -84,6 +161,12 @@ public final class IrisModdedAPI { engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data); } + /** + * Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range + * {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent. + * + * @throws IllegalStateException if the engine's mantle has already been closed + */ public static void deleteMantleData(ServerLevel level, int x, int y, int z, Class type) { Engine engine = getEngine(level); if (engine == null) { @@ -92,6 +175,14 @@ public final class IrisModdedAPI { engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type); } + /** + * Declares that mantle slices of {@code sliceType} must be kept rather than discarded. + *

+ * Iris drops slices it does not need once a region's generation data has served its purpose. Any type a mod + * writes with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be + * declared here first. Registration is by canonical class name, process-wide across every Iris world, and + * cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored. + */ public static void retainMantleDataForSlice(Class sliceType) { if (sliceType == null) { return; @@ -99,10 +190,31 @@ public final class IrisModdedAPI { WorldMaintenance.retainMantleDataForSlice(sliceType.getCanonicalName()); } + /** + * Registers a custom content provider imperatively, for mods that would rather call Iris than ship a + * {@link java.util.ServiceLoader} entry. + *

+ * Providers are keyed by {@link ModdedDataProvider#modId()}; a second registration under an id already present + * is logged and ignored. {@link ModdedDataProvider#init()} runs during this call, and a throwable it raises is + * logged rather than propagated. A null {@code provider} is ignored. + *

+ * Ordering matters: Iris only consults providers registered before a pack resolves the block in question, so + * register during mod setup. Registering after Iris's own {@link java.util.ServiceLoader} discovery is + * supported; registering after a world has generated is not - blocks already resolved are not revisited. + */ public static void registerProvider(ModdedDataProvider provider) { ModdedCustomContentRegistry.register(provider); } + /** + * Maps a custom {@code namespace:key} onto a fixed vanilla block state, for mods that only need a static alias + * and no provider class. + *

+ * {@code state} is a block state string in the same syntax packs use, for example + * {@code minecraft:oak_log[axis=y]}, and is parsed immediately: an unparseable state or an invalid identifier + * is logged and the registration is dropped, so a typo shows up at startup rather than as missing blocks. + * Aliases take precedence over provider lookups for the same key. Null arguments are ignored. + */ public static void registerCustomBlockData(String namespace, String key, String state) { ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java index 1d1b67bb8..f0cc954ae 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockData.java @@ -22,15 +22,39 @@ import net.minecraft.world.level.block.state.BlockState; import java.util.Objects; +/** + * A provider's answer to a block lookup: the state to write, and whether the provider wants a second pass once the + * chunk is loaded. + *

+ * Immutable. Returned from {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}; + * construct with {@link #direct(BlockState)} or {@link #deferred(BlockState)} rather than the canonical constructor. + * + * @param state the block state Iris writes. Never null + * @param deferredPlacement whether {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} + * should run for this position after the chunk is loaded + */ public record ModdedBlockData(BlockState state, boolean deferredPlacement) { + /** + * @throws NullPointerException if {@code state} is null + */ public ModdedBlockData { Objects.requireNonNull(state); } + /** + * The state is final - Iris writes it during generation and does nothing further. + */ public static ModdedBlockData direct(BlockState state) { return new ModdedBlockData(state, false); } + /** + * {@code state} is a placeholder written during generation; the provider finishes the job in + * {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} once the chunk is loaded. Use + * when the real block needs a level - a block entity, neighbour state, or mod registries not available on a + * generation thread. Pick a placeholder with the same shape and occlusion as the final block so terrain around + * it generates correctly. + */ public static ModdedBlockData deferred(BlockState state) { return new ModdedBlockData(state, true); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java index 55b4076cc..dc5110b31 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedBlockPlacementContext.java @@ -27,6 +27,22 @@ import net.minecraft.world.level.block.state.BlockState; import java.util.Map; import java.util.Objects; +/** + * Everything a provider needs to finish a deferred block placement, handed to + * {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} on the server thread. + *

+ * Immutable, and constructed by Iris rather than by mods. {@code state} is defensively copied; {@code position} is + * already immutable. Because delivery is on the server thread with the chunk loaded, it is safe to write blocks, + * attach block entities and read neighbours from here. + * + * @param engine the Iris engine for this level. Internal Iris type - treat it as an opaque token + * @param level the level to write into. Never null + * @param position the block the placeholder was written at. Never null + * @param blockId the identifier the pack named, without state properties. Never null + * @param blockState the state currently at {@code position} - normally the placeholder returned as deferred, though + * another provider or a later generation stage may have replaced it. Never null + * @param state the {@code [prop=value]} pairs from the pack's key, possibly empty. Never null; unmodifiable + */ public record ModdedBlockPlacementContext( Engine engine, ServerLevel level, @@ -34,6 +50,9 @@ public record ModdedBlockPlacementContext( Identifier blockId, Map state, BlockState blockState) { + /** + * @throws NullPointerException if any component is null + */ public ModdedBlockPlacementContext { Objects.requireNonNull(engine); Objects.requireNonNull(level); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java index 299673b1a..af3f37bd5 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedCustomContentRegistry.java @@ -37,6 +37,22 @@ import java.util.ServiceLoader; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +/** + * Registry of {@link ModdedDataProvider} instances and static block-data aliases, and the resolution path Iris + * itself calls into. + *

+ * Mods should go through {@link IrisModdedAPI#registerProvider(ModdedDataProvider)} and + * {@link IrisModdedAPI#registerCustomBlockData(String, String, String)} rather than calling this class directly; + * the resolution methods here are Iris internals and are public only because the adapter's generation code lives in + * another package. + *

+ * Threading. Mutation ({@link #register(ModdedDataProvider)}, + * {@link #registerCustomBlockData(String, String, String)}, {@link #discover()}) is serialized on the class + * monitor. Resolution ({@link #resolveBlock(String)}, {@link #spawnMob}, {@link #processBlockPlacement}) is lock + * free over a copy-on-write provider list and a concurrent alias map, so it runs on generation threads. Every + * resolution method catches provider throwables, logs them against the provider's mod id, and continues with the + * next provider. + */ public final class ModdedCustomContentRegistry { private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final List PROVIDERS = new CopyOnWriteArrayList<>(); @@ -47,6 +63,11 @@ public final class ModdedCustomContentRegistry { private ModdedCustomContentRegistry() { } + /** + * Registers a static {@code namespace:key} to block-state alias. Invalid identifiers and unparseable states are + * logged and dropped; null arguments are ignored. See + * {@link IrisModdedAPI#registerCustomBlockData(String, String, String)}. + */ public static synchronized void registerCustomBlockData(String namespace, String key, String state) { if (namespace == null || key == null || state == null) { return; @@ -72,6 +93,11 @@ public final class ModdedCustomContentRegistry { LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state); } + /** + * Registers a provider, rejecting a duplicate {@link ModdedDataProvider#modId()} with a warning and ignoring + * null. {@link ModdedDataProvider#init()} runs here; a throwable it raises is logged, not propagated. See + * {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. + */ public static synchronized void register(ModdedDataProvider provider) { if (provider == null) { return; @@ -96,6 +122,15 @@ public final class ModdedCustomContentRegistry { LOGGER.info("Iris registered custom content provider '{}'", provider.modId()); } + /** + * Runs {@link ServiceLoader} discovery for {@link ModdedDataProvider} against Iris's own class loader, once per + * process. Called during Iris mod initialization; a second call is a no-op that returns an inert handle. + *

+ * All-or-nothing: a provider whose {@link ModdedDataProvider#init()} throws aborts the pass, restores the + * previous provider and alias state, logs the failing provider's identity, and rethrows. + * + * @return a handle whose {@link Discovery#rollback()} undoes this pass, used by the bootstrap's rollback chain + */ public static synchronized Discovery discover() { if (scanned) { return Discovery.unchanged(); @@ -110,9 +145,12 @@ public final class ModdedCustomContentRegistry { boolean previousDiscoveryComplete = scanned; DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks); discoveryBatch = batch; + ModdedDataProvider failingProvider = null; try { for (ModdedDataProvider provider : discoveredProviders) { + failingProvider = provider; batch.add(provider); + failingProvider = null; } PROVIDERS.addAll(batch.additions); CUSTOM_BLOCKS.putAll(batch.customBlocks); @@ -130,7 +168,8 @@ public final class ModdedCustomContentRegistry { failure.addSuppressed(rollbackFailure); } } - LOGGER.error("Iris custom content provider discovery failed", failure); + LOGGER.warn("Iris custom content provider discovery failed at {}", + providerIdentity(failingProvider), failure); if (failure instanceof RuntimeException runtimeException) { throw runtimeException; } @@ -143,6 +182,19 @@ public final class ModdedCustomContentRegistry { } } + private static String providerIdentity(ModdedDataProvider provider) { + if (provider == null) { + return "the provider service loader"; + } + String className = provider.getClass().getName(); + try { + String modId = provider.modId(); + return modId == null || modId.isBlank() ? className : "provider '" + modId + "' (" + className + ")"; + } catch (Throwable identityFailure) { + return className; + } + } + static synchronized boolean hasProvider(String modId) { for (ModdedDataProvider provider : PROVIDERS) { if (Objects.equals(provider.modId(), modId)) { @@ -156,10 +208,19 @@ public final class ModdedCustomContentRegistry { return scanned; } + /** + * Whether any provider or alias is registered. Iris checks this to skip custom resolution entirely on a server + * with no integrating mods. + */ public static boolean hasProviders() { return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty(); } + /** + * Resolves a pack block key against aliases first, then each ready provider that claims it, in registration + * order. {@code key} may carry {@code [prop=value]} properties, which are parsed and passed along. Returns null + * when nothing claims it, which lets the caller fall back to air. Called from generation threads. + */ public static ModdedBlockData resolveBlock(String key) { if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) { return null; @@ -192,6 +253,11 @@ public final class ModdedCustomContentRegistry { return null; } + /** + * Delivers a deferred placement to the first ready provider claiming {@code key}; later providers are not + * consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the + * server thread with the chunk loaded. + */ public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) { Identifier base = parseIdentifier(key); if (base == null) { @@ -214,6 +280,10 @@ public final class ModdedCustomContentRegistry { LOGGER.warn("Iris deferred custom block placement has no provider for {}", key); } + /** + * Asks each ready provider claiming {@code key} to spawn a custom entity, returning the first non-null result. + * Null when no provider claims it or every attempt declined. Called on the server thread. + */ public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) { if (PROVIDERS.isEmpty() || level == null || key == null) { return null; @@ -276,6 +346,10 @@ public final class ModdedCustomContentRegistry { scanned = discoveryComplete; } + /** + * Undo handle for one {@link #discover()} pass, so a failure later in Iris's bootstrap can restore the registry + * to its pre-discovery state. + */ public static final class Discovery { private final List providers; private final Map customBlocks; @@ -294,6 +368,10 @@ public final class ModdedCustomContentRegistry { return new Discovery(List.of(), Map.of(), true, false); } + /** + * Restores the providers and aliases captured before the pass. Idempotent; a no-op on a handle from a + * discovery that did not run. + */ public synchronized void rollback() { if (!active) { return; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java index b1907267e..da6a147df 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataProvider.java @@ -25,28 +25,91 @@ import net.minecraft.world.entity.Entity; import java.util.Collection; import java.util.Map; +/** + * Extension point letting a mod resolve its own blocks, items and entities for Iris packs, so a pack can name + * {@code yourmod:something} and have it placed. + *

+ * Discovered through {@link java.util.ServiceLoader} at Iris mod initialization, or registered imperatively with + * {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. For ServiceLoader discovery, ship + * {@code META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider} listing the implementation's binary + * name; the class needs a public no-argument constructor. + *

+ * Threading. Implementations must be thread-safe. + * {@link #getBlockData(Identifier, Map)} is called from generation threads, potentially many at once, for every + * unresolved key a pack names - it must be fast and must not touch world state. + * {@link #processBlockPlacement(ModdedBlockPlacementContext)} and + * {@link #spawnMob(ServerLevel, double, double, double, Identifier)} are called on the server thread, where + * touching the level is safe. + *

+ * Failure handling. Iris catches throwables from every callback except {@link #init()} during + * ServiceLoader discovery, logs them against {@link #modId()}, and carries on with the remaining providers - one + * broken provider does not stop world generation. A throwable from {@link #init()} during discovery aborts + * discovery and rolls back every provider registered in that pass. + */ public interface ModdedDataProvider { + /** + * The owning mod's id. Used as the provider's identity: duplicates are rejected, and it labels every log line + * Iris emits about this provider. Must be non-null and stable; returning null aborts discovery. + */ String modId(); + /** + * Whether this provider can answer lookups yet. Iris skips a provider that reports false rather than treating + * it as absent, so a provider whose registries populate late can gate itself instead of returning wrong + * answers. Defaults to true. + */ default boolean isReady() { return true; } + /** + * Every identifier this provider can supply for {@code type}. Used for command suggestion and pack tooling, not + * on the resolution path - {@link #isValidProvider(Identifier, ModdedDataType)} decides that. Return an empty + * collection rather than null. + */ Collection getTypes(ModdedDataType type); + /** + * Whether this provider claims {@code id} for {@code type}. Called before every resolution callback, on + * generation threads, so keep it to a set lookup. A cheap namespace check is usually enough. + */ boolean isValidProvider(Identifier id, ModdedDataType type); + /** + * Resolves a claimed block identifier into a concrete block state. + *

+ * {@code state} holds the {@code [prop=value]} pairs from the pack's key, already parsed and possibly empty; + * never null. Return null to decline, in which case Iris tries the next provider and finally falls back to air. + * Return {@link ModdedBlockData#deferred(net.minecraft.world.level.block.state.BlockState)} when the real block + * needs a loaded level - Iris then writes the placeholder state and calls + * {@link #processBlockPlacement(ModdedBlockPlacementContext)} later. Called from generation threads. + */ default ModdedBlockData getBlockData(Identifier blockId, Map state) { return null; } + /** + * Finishes a deferred placement once the chunk is loaded: swap in the real block, attach a block entity, seed + * NBT. + *

+ * Called on the server thread, once per deferred position, by the first provider that claims the identifier - + * later providers are not consulted for that position. Only fires for states returned as deferred. + */ default void processBlockPlacement(ModdedBlockPlacementContext context) { } + /** + * Spawns a claimed custom entity at the given position. Return null to decline and let the next provider try. + * Called on the server thread from Iris's entity spawning. + */ default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) { return null; } + /** + * One-time setup, called by Iris immediately after this provider is accepted. A throwable raised here aborts + * ServiceLoader discovery; when registered imperatively it is logged and the provider stays registered. + */ default void init() { } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java index 1c0e75baf..7a0677fb3 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/api/ModdedDataType.java @@ -18,8 +18,16 @@ package art.arcane.iris.modded.api; +/** + * The kinds of custom content a {@link ModdedDataProvider} can claim. + *

+ * Constants may be added. Switch expressions over this enum need a {@code default} arm. + */ public enum ModdedDataType { + /** Block states, resolved through {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}. */ BLOCK, + /** Item types, claimed for loot and pack tooling. */ ITEM, + /** Entity types, spawned through {@link ModdedDataProvider#spawnMob(net.minecraft.server.level.ServerLevel, double, double, double, net.minecraft.resources.Identifier)}. */ ENTITY } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java index be4743429..9b34b9992 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java @@ -20,20 +20,8 @@ package art.arcane.iris.modded.command; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.localization.IrisMessages; -import art.arcane.iris.core.gui.GuiHost; -import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.pack.PackDownloader; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.framework.GenerationSessionException; -import art.arcane.iris.engine.framework.GenerationSessionLease; -import art.arcane.iris.engine.framework.IrisStructureLocator; -import art.arcane.iris.engine.framework.Locator; -import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; -import art.arcane.iris.engine.framework.WrongEngineBroException; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisNativeStructureDecision; -import art.arcane.iris.engine.object.NativeStructureGenerationStatus; -import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedDimensionManager; import art.arcane.iris.modded.ModdedEngineBootstrap; @@ -42,64 +30,28 @@ import art.arcane.iris.modded.ModdedPackInstaller; import art.arcane.iris.modded.ModdedScheduler; import art.arcane.iris.modded.ModdedWorldgenIds; import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.math.Position2; -import com.mojang.datafixers.util.Pair; import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.arguments.BoolArgumentType; -import com.mojang.brigadier.arguments.IntegerArgumentType; -import com.mojang.brigadier.arguments.LongArgumentType; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.ArgumentBuilder; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; -import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.tree.LiteralCommandNode; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; -import net.minecraft.commands.SharedSuggestionProvider; -import net.minecraft.commands.arguments.DimensionArgument; -import net.minecraft.commands.arguments.EntityArgument; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Holder; -import net.minecraft.core.HolderSet; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.entity.Relative; import net.minecraft.world.level.chunk.ChunkGenerator; -import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.level.levelgen.structure.Structure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.awt.Desktop; -import java.io.File; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.ModdedCommandMessages; @@ -107,422 +59,20 @@ import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class IrisModdedCommands { private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); - private static final long LOCATE_TIMEOUT_MS = 120000L; - private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100; - private static final ConcurrentHashMap> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>(); - private static final SuggestionProvider BIOME_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder); - private static final SuggestionProvider REGION_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder); - private static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder); - private static final SuggestionProvider STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder); - private static final SuggestionProvider POI_TYPES = (CommandContext context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder); - static final SuggestionProvider PACK_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestPackNames(context, builder); - private static final SuggestionProvider DIMENSION_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder); + static final SuggestionProvider PACK_NAMES = ModdedCommandSuggestions.PACK_NAMES; private IrisModdedCommands() { } public static void register(CommandDispatcher dispatcher) { - LiteralCommandNode root = dispatcher.register(rootTree()); + LiteralCommandNode root = dispatcher.register(ModdedCommandTree.rootTree()); dispatcher.register(Commands.literal("ir").redirect(root)); dispatcher.register(Commands.literal("irs").redirect(root)); IrisLogging.info("Iris /iris command tree registered"); } - private static LiteralArgumentBuilder rootTree() { - LiteralArgumentBuilder root = Commands.literal("iris"); - - root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")); - root.then(helpTree()); - - root.then(Commands.literal("version") - .executes((CommandContext context) -> version(context.getSource()))); - - root.then(Commands.literal("info").requires(GATE) - .executes((CommandContext context) -> info(context.getSource(), null)) - .then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(DIMENSION_NAMES) - .executes((CommandContext context) -> info(context.getSource(), StringArgumentType.getString(context, "dimension"))))); - - root.then(ModdedWhatCommands.tree()); - - root.then(teleportTree("teleport")); - root.then(teleportTree("tp")); - - root.then(Commands.literal("evacuate").requires(GATE) - .executes((CommandContext context) -> evacuate(context.getSource(), null)) - .then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES) - .executes((CommandContext context) -> evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension"))))); - - root.then(Commands.literal("debug").requires(GATE) - .executes((CommandContext context) -> debug(context.getSource()))); - - root.then(Commands.literal("reload").requires(GATE) - .executes((CommandContext context) -> reload(context.getSource()))); - root.then(Commands.literal("height").requires(GATE) - .executes((CommandContext context) -> height(context.getSource()))); - root.then(Commands.literal("worlds").requires(GATE) - .executes((CommandContext context) -> info(context.getSource(), null))); - root.then(Commands.literal("accesslist").requires(GATE) - .executes((CommandContext context) -> info(context.getSource(), null))); - - root.then(gotoTree("goto")); - root.then(gotoTree("find")); - - root.then(Commands.literal("seed").requires(GATE) - .executes((CommandContext context) -> seed(context.getSource()))); - - root.then(goldenhashTree("goldenhash")); - root.then(goldenhashTree("gold")); - - root.then(downloadTree("download")); - root.then(downloadTree("dl")); - - root.then(metricsTree("metrics")); - root.then(metricsTree("measure")); - - root.then(regenTree("regen")); - root.then(regenTree("rg")); - - root.then(pregenTree("pregen")); - root.then(pregenTree("pregenerate")); - - root.then(Commands.literal("wand").requires(GATE) - .executes((CommandContext context) -> ModdedObjectCommands.giveWand(context.getSource()))); - root.then(Commands.literal("dust").requires(GATE) - .executes((CommandContext context) -> ModdedObjectCommands.giveDust(context.getSource()))); - root.then(Commands.literal("d").requires(GATE) - .executes((CommandContext context) -> ModdedObjectCommands.giveDust(context.getSource()))); - root.then(ModdedObjectCommands.tree("object")); - root.then(ModdedObjectCommands.tree("o")); - root.then(editTree()); - - root.then(createTree("create")); - root.then(createTree("c")); - - root.then(ModdedStudioCommands.tree("studio")); - root.then(ModdedStudioCommands.tree("std")); - root.then(ModdedStudioCommands.tree("s")); - root.then(ModdedPackCommands.tree("pack")); - root.then(ModdedPackCommands.tree("pk")); - root.then(ModdedWorldCommands.tree("world")); - root.then(ModdedWorldCommands.tree("w")); - root.then(ModdedDatapackCommands.tree("datapack")); - root.then(ModdedDatapackCommands.tree("datapacks")); - root.then(ModdedDatapackCommands.tree("dp")); - root.then(ModdedStructureCommands.tree("structure")); - root.then(ModdedStructureCommands.tree("struct")); - root.then(ModdedStructureCommands.tree("str")); - root.then(ModdedDeveloperCommands.tree("developer")); - root.then(ModdedDeveloperCommands.tree("dev")); - - return root; - } - - private static LiteralArgumentBuilder createTree(String name) { - return Commands.literal(name).requires(GATE) - .then(Commands.argument("name", StringArgumentType.word()) - .executes((CommandContext context) -> - ModdedWorldCommands.createWorld( - context.getSource(), - StringArgumentType.getString(context, "name"), - "overworld", - 1337L)) - .then(Commands.argument("pack", StringArgumentType.string()).suggests(PACK_NAMES) - .executes((CommandContext context) -> ModdedWorldCommands.createWorld(context.getSource(), - StringArgumentType.getString(context, "name"), - StringArgumentType.getString(context, "pack"), - 1337L)) - .then(Commands.argument("seed", LongArgumentType.longArg()) - .executes((CommandContext context) -> ModdedWorldCommands.createWorld(context.getSource(), - StringArgumentType.getString(context, "name"), - StringArgumentType.getString(context, "pack"), - LongArgumentType.getLong(context, "seed")))))); - } - - private static LiteralArgumentBuilder teleportTree(String name) { - return Commands.literal(name).requires(GATE) - .then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES) - .executes((CommandContext context) -> - tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null)) - .then(Commands.argument("player", EntityArgument.player()) - .executes((CommandContext context) -> - tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), - EntityArgument.getPlayer(context, "player"))))); - } - - private static LiteralArgumentBuilder helpTree() { - return Commands.literal("help") - .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")) - .then(Commands.argument("section", StringArgumentType.greedyString()) - .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section")))); - } - - private static LiteralArgumentBuilder downloadTree(String name) { - return Commands.literal(name).requires(GATE) - .then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES) - .executes((CommandContext context) -> - download(context.getSource(), - StringArgumentType.getString(context, "pack"), "stable", false)) - .then(Commands.literal("force") - .executes((CommandContext context) -> - download(context.getSource(), - StringArgumentType.getString(context, "pack"), "stable", true))) - .then(Commands.argument("overwrite", BoolArgumentType.bool()) - .executes((CommandContext context) -> - download(context.getSource(), - StringArgumentType.getString(context, "pack"), "stable", - BoolArgumentType.getBool(context, "overwrite")))) - .then(Commands.argument("branch", StringArgumentType.word()) - .executes((CommandContext context) -> - download(context.getSource(), - StringArgumentType.getString(context, "pack"), - StringArgumentType.getString(context, "branch"), false)) - .then(Commands.literal("force") - .executes((CommandContext context) -> - download(context.getSource(), - StringArgumentType.getString(context, "pack"), - StringArgumentType.getString(context, "branch"), true))) - .then(Commands.argument("overwrite", BoolArgumentType.bool()) - .executes((CommandContext context) -> - download(context.getSource(), - StringArgumentType.getString(context, "pack"), - StringArgumentType.getString(context, "branch"), - BoolArgumentType.getBool(context, "overwrite")))))); - } - - private static LiteralArgumentBuilder metricsTree(String name) { - return Commands.literal(name).requires(GATE) - .executes((CommandContext context) -> metrics(context.getSource())); - } - - private static LiteralArgumentBuilder regenTree(String name) { - return Commands.literal(name).requires(GATE) - .executes((CommandContext context) -> regen(context.getSource(), 0)) - .then(Commands.argument("radius", IntegerArgumentType.integer(0, 64)) - .executes((CommandContext context) -> regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius")))); - } - - private static LiteralArgumentBuilder gotoTree(String name) { - return Commands.literal(name).requires(GATE) - .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) - .then(Commands.literal("biome") - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS) - .executes((CommandContext context) -> gotoBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("region") - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(REGION_KEYS) - .executes((CommandContext context) -> gotoRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("object") - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) - .executes((CommandContext context) -> gotoObject(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("structure") - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(STRUCTURE_KEYS) - .executes((CommandContext context) -> gotoStructure(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("poi") - .then(Commands.argument("type", StringArgumentType.greedyString()).suggests(POI_TYPES) - .executes((CommandContext context) -> gotoPoi(context.getSource(), StringArgumentType.getString(context, "type"))))); - } - - private static LiteralArgumentBuilder pregenTree(String name) { - RequiredArgumentBuilder radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000)) - .executes((CommandContext context) -> pregenStart(context, false, false, false, false, false)); - attachPregenCenter(radius, false); - attachPregenFlags(radius, false, false, false, false, false); - RequiredArgumentBuilder dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES) - .executes((CommandContext context) -> pregenStart(context, true, false, false, false, false)); - attachPregenCenter(dimension, true); - attachPregenFlags(dimension, true, false, false, false, false); - radius.then(dimension); - - return Commands.literal(name).requires(GATE) - .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) - .then(Commands.literal("start") - .then(radius)) - .then(Commands.literal("stop") - .executes((CommandContext context) -> pregenStop(context.getSource()))) - .then(Commands.literal("x") - .executes((CommandContext context) -> pregenStop(context.getSource()))) - .then(Commands.literal("pause") - .executes((CommandContext context) -> pregenPause(context.getSource()))) - .then(Commands.literal("resume") - .executes((CommandContext context) -> pregenPause(context.getSource()))) - .then(Commands.literal("status") - .executes((CommandContext context) -> pregenStatus(context.getSource()))); - } - - private static void attachPregenCenter(ArgumentBuilder node, boolean withDimension) { - RequiredArgumentBuilder z = Commands.argument("z", IntegerArgumentType.integer()) - .executes((CommandContext context) -> pregenStart(context, withDimension, true, false, false, false)); - attachPregenFlags(z, withDimension, true, false, false, false); - node.then(Commands.literal("at") - .then(Commands.argument("x", IntegerArgumentType.integer()) - .then(z))); - } - - private static void attachPregenFlags(ArgumentBuilder node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) { - if (!gui) { - node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache)); - } - if (!sync) { - node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache)); - } - if (!nocache) { - node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true)); - } - } - - private static LiteralArgumentBuilder pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) { - LiteralArgumentBuilder flag = Commands.literal(name) - .executes((CommandContext context) -> pregenStart(context, withDimension, withCenter, gui, sync, nocache)); - attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache); - return flag; - } - - private static LiteralArgumentBuilder goldenhashTree(String name) { - LiteralArgumentBuilder radiusAndThreads = Commands.literal(name).requires(GATE) - .executes((CommandContext context) -> goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO)); - attachModes(radiusAndThreads, (CommandContext context) -> 8, (CommandContext context) -> 8); - - com.mojang.brigadier.builder.RequiredArgumentBuilder radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256)) - .executes((CommandContext context) -> goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO)); - attachModes(radius, (CommandContext context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext context) -> 8); - - com.mojang.brigadier.builder.RequiredArgumentBuilder threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64)) - .executes((CommandContext context) -> goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO)); - attachModes(threads, (CommandContext context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext context) -> IntegerArgumentType.getInteger(context, "threads")); - - radius.then(threads); - radiusAndThreads.then(radius); - return radiusAndThreads; - } - - private interface IntExtractor { - int extract(CommandContext context); - } - - private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder node, IntExtractor radius, IntExtractor threads) { - node.then(Commands.literal("capture") - .executes((CommandContext context) -> goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE))); - node.then(Commands.literal("verify") - .executes((CommandContext context) -> goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY))); - } - - private static LiteralArgumentBuilder editTree() { - return Commands.literal("edit").requires(GATE) - .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "edit")) - .then(Commands.literal("biome") - .executes((CommandContext context) -> editBiome(context.getSource(), null)) - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS) - .executes((CommandContext context) -> editBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("b") - .executes((CommandContext context) -> editBiome(context.getSource(), null)) - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS) - .executes((CommandContext context) -> editBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("region") - .executes((CommandContext context) -> editRegion(context.getSource(), null)) - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(REGION_KEYS) - .executes((CommandContext context) -> editRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("r") - .executes((CommandContext context) -> editRegion(context.getSource(), null)) - .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(REGION_KEYS) - .executes((CommandContext context) -> editRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) - .then(Commands.literal("dimension") - .executes((CommandContext context) -> editDimension(context.getSource()))) - .then(Commands.literal("d") - .executes((CommandContext context) -> editDimension(context.getSource()))); - } - - private static int editBiome(CommandSourceStack source, String key) { - Engine engine = engineFor(source.getLevel()); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS)); - return 0; - } - IrisBiome biome; - if (key == null || key.isBlank()) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY)); - return 0; - } - BlockPos pos = player.blockPosition(); - try { - biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ()); - } catch (Throwable e) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); - return 0; - } - } else { - biome = engine.getData().getBiomeLoader().load(key.trim()); - if (biome == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, MessageArgument.untrusted("key", key))); - return 0; - } - } - return openJson(source, biome); - } - - private static int editRegion(CommandSourceStack source, String key) { - Engine engine = engineFor(source.getLevel()); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2)); - return 0; - } - IrisRegion region; - if (key == null || key.isBlank()) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY)); - return 0; - } - BlockPos pos = player.blockPosition(); - try { - region = engine.getRegion(pos.getX(), pos.getZ()); - } catch (Throwable e) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); - return 0; - } - } else { - region = engine.getData().getRegionLoader().load(key.trim()); - if (region == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION, MessageArgument.untrusted("key", key))); - return 0; - } - } - return openJson(source, region); - } - - private static int editDimension(CommandSourceStack source) { - Engine engine = engineFor(source.getLevel()); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3)); - return 0; - } - return openJson(source, engine.getDimension()); - } - - private static int openJson(CommandSourceStack source, IrisRegistrant registrant) { - if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, MessageArgument.untrusted("value", ModdedGuiHost.guiUnavailableReason()))); - return 0; - } - if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM)); - return 0; - } - File file = registrant.getLoadFile(); - try { - Desktop.getDesktop().open(file); - } catch (Throwable e) { - LOGGER.error("Iris edit failed to open {}", file, e); - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); - return 0; - } - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", registrant.getTypeName()), MessageArgument.untrusted("value2", file.getName()))); - return 1; - } - - private static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) { + static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) { ServerPlayer player = target != null ? target : source.getPlayer(); if (player == null) { fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_PLAYER_IRIS_TP_DIMENSION_PLAYER)); @@ -541,7 +91,7 @@ public final class IrisModdedCommands { return 1; } - private static int evacuate(CommandSourceStack source, ServerLevel target) { + static int evacuate(CommandSourceStack source, ServerLevel target) { MinecraftServer server = source.getServer(); ServerLevel level = target != null ? target : source.getLevel(); if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) { @@ -558,7 +108,7 @@ public final class IrisModdedCommands { return 1; } - private static int debug(CommandSourceStack source) { + static int debug(CommandSourceStack source) { boolean to = !IrisSettings.get().getGeneral().isDebug(); IrisSettings.get().getGeneral().setDebug(to); IrisSettings.get().forceSave(); @@ -566,7 +116,7 @@ public final class IrisModdedCommands { return 1; } - private static int reload(CommandSourceStack source) { + static int reload(CommandSourceStack source) { if (IrisSettings.settings != null) { IrisSettings.invalidate(); } @@ -587,7 +137,7 @@ public final class IrisModdedCommands { return 0; } - private static int height(CommandSourceStack source) { + static int height(CommandSourceStack source) { ServerLevel level = source.getLevel(); IrisModdedCommands.ok(source, IrisLanguage.plain( RuntimeUiMessages.WORLD_HEIGHT_RANGE, @@ -599,7 +149,7 @@ public final class IrisModdedCommands { return 1; } - private static int regen(CommandSourceStack source, int radius) { + static int regen(CommandSourceStack source, int radius) { ServerPlayer player = source.getPlayer(); if (player == null) { fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS)); @@ -619,78 +169,14 @@ public final class IrisModdedCommands { return 1; } - private static int pregenStart(CommandContext context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException { - CommandSourceStack source = context.getSource(); - int radius = IntegerArgumentType.getInteger(context, "radius"); - int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0; - int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0; - ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - if (withDimension) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()))); - } else { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("radius", radius))); - } - return 0; - } - boolean showGui = gui && ModdedGuiHost.isGuiLaunchable(); - if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS)); - return 0; - } - ModdedPregenBossBar.begin(source.getPlayer()); - String guiNote; - if (!gui) { - guiNote = ""; - } else if (showGui) { - guiNote = " A progress map window is opening on the server display."; - } else { - guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")"; - } - String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache)."); - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", (radius * 2)), MessageArgument.untrusted("value3", (radius * 2)), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ), MessageArgument.untrusted("modeNote", modeNote), MessageArgument.untrusted("guiNote", guiNote))); - return 1; - } - - private static int pregenStop(CommandSourceStack source) { - if (ModdedPregenJob.stop()) { - ModdedPregenBossBar.clear(); - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION)); - return 1; - } - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP)); - return 0; - } - - private static int pregenPause(CommandSourceStack source) { - Boolean paused = ModdedPregenJob.pauseResume(); - if (paused == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME)); - return 0; - } - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, MessageArgument.trusted("value", IrisLanguage.plain(paused.booleanValue() ? RuntimeUiMessages.STATUS_PAUSED_LOWER : RuntimeUiMessages.STATUS_RUNNING_LOWER)))); - return 1; - } - - private static int pregenStatus(CommandSourceStack source) { - Component status = ModdedPregenJob.statusComponent(); - if (status == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK)); - return 0; - } - ok(source, status); - return 1; - } - - private static int version(CommandSourceStack source) { + static int version(CommandSourceStack source) { ModdedLoader loader = ModdedEngineBootstrap.loader(); int engines = engineCount(source.getServer()); ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IRIS_BY_VOLMIT_SOFTWARE_ON_MINECRAFT_IRIS_DIMENSION_S, MessageArgument.untrusted("value", loader.modVersion()), MessageArgument.untrusted("value2", loader.platformName()), MessageArgument.untrusted("value3", loader.minecraftVersion()), MessageArgument.untrusted("engines", engines))); return 1; } - private static int info(CommandSourceStack source, String filter) { + static int info(CommandSourceStack source, String filter) { MinecraftServer server = source.getServer(); List lines = new ArrayList<>(); int total = 0; @@ -737,456 +223,7 @@ public final class IrisModdedCommands { return 1; } - private static int gotoBiome(CommandSourceStack source, String key) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3)); - return 0; - } - ServerLevel level = source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8)); - return 0; - } - IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim()); - if (biome == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key))); - return 0; - } - locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey()); - return 1; - } - - private static int gotoRegion(CommandSourceStack source, String key) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4)); - return 0; - } - ServerLevel level = source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9)); - return 0; - } - IrisRegion region = engine.getData().getRegionLoader().load(key.trim()); - if (region == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key))); - return 0; - } - if (!engine.getDimension().getRegions().contains(region.getLoadKey())) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey()))); - return 0; - } - locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey()); - return 1; - } - - private static int gotoObject(CommandSourceStack source, String keyRaw) { - ServerLevel level = source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10)); - return 0; - } - String key = keyRaw.trim(); - if (!engine.hasObjectPlacement(key)) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length))); - return 0; - } - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length))); - return 0; - } - locate(source, level, engine, player, Locator.object(key), "object " + key); - return 1; - } - - private static int gotoStructure(CommandSourceStack source, String keyRaw) { - ServerLevel level = source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11)); - return 0; - } - String key = keyRaw.trim(); - if (key.isEmpty()) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE)); - return 0; - } - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5)); - return 0; - } - Optional resolved = resolveNativeStructure(source, level, engine, key); - if (resolved.isEmpty()) { - if (IrisStructureLocator.isPlaced(engine, key)) { - locateIrisStructure(source, level, engine, player, key); - return 1; - } - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, MessageArgument.untrusted("key", key))); - return 0; - } - NativeStructureTarget target = resolved.get(); - IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false); - if (!decision.generate() - && decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) { - fail(source, NativeStructureGenerationPolicy.generationStatusMessage( - target.key(), decision.status())); - return 0; - } - if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { - locateIrisStructure(source, level, engine, player, target.key()); - return 1; - } - if (target.availability() != NativeStructureAvailability.AVAILABLE) { - fail(source, nativeUnavailableMessage(target.key(), target.availability())); - return 0; - } - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS))); - runNativeStructureLocate(source, level, player, target); - return 1; - } - - private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine, - ServerPlayer player, String key) { - MinecraftServer server = source.getServer(); - int blockX = player.blockPosition().getX(); - int blockZ = player.blockPosition().getZ(); - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, MessageArgument.untrusted("key", key))); - Thread thread = new Thread(() -> { - try { - IrisStructureLocator.LocateResult result = - IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024); - if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { - server.execute(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key)))); - return; - } - if (!result.found()) { - server.execute(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, MessageArgument.untrusted("key", key)))); - return; - } - int targetX = result.originX(); - int targetY = result.baseY() + 2; - int targetZ = result.originZ(); - server.execute(() -> teleportToStructure(source, level, player, targetX, targetY, targetZ, - "Iris-placed structure " + key)); - } catch (Throwable e) { - LOGGER.error("Iris structure locate failed for {}", key, e); - server.execute(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())))); - } - }, "Iris Structure Locator"); - thread.setDaemon(true); - thread.start(); - } - - private static void runNativeStructureLocate(CommandSourceStack source, ServerLevel level, - ServerPlayer player, NativeStructureTarget target) { - MinecraftServer server = source.getServer(); - Runnable locateTask = () -> locateNativeStructure(source, level, player, target); - if (Thread.currentThread() == server.getRunningThread()) { - locateTask.run(); - return; - } - server.execute(locateTask); - } - - private static void locateNativeStructure(CommandSourceStack source, ServerLevel level, - ServerPlayer player, NativeStructureTarget target) { - try { - ChunkGenerator generator = level.getChunkSource().getGenerator(); - Pair> found = generator.findNearestMapStructure( - level, - HolderSet.direct(target.holder()), - player.blockPosition(), - NATIVE_STRUCTURE_LOCATE_RADIUS, - false); - if (found == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS))); - return; - } - BlockPos position = found.getFirst(); - int targetX = position.getX(); - int targetZ = position.getZ(); - level.getChunk(targetX >> 4, targetZ >> 4); - int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, targetX, targetZ) + 1; - int targetY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, surfaceY)); - teleportToStructure(source, level, player, targetX, targetY, targetZ, - "native structure " + target.key()); - } catch (Throwable e) { - LOGGER.error("Native structure locate failed for {}", target.key(), e); - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); - } - } - - private static void teleportToStructure(CommandSourceStack source, ServerLevel level, ServerPlayer player, - int targetX, int targetY, int targetZ, String label) { - if (player.hasDisconnected() || player.isRemoved()) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED)); - return; - } - if (player.level() != level) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN)); - return; - } - level.getChunk(targetX >> 4, targetZ >> 4); - int clampedY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, targetY)); - boolean teleported = player.teleportTo(level, targetX + 0.5D, clampedY, targetZ + 0.5D, - Set.of(), player.getYRot(), player.getXRot(), false); - if (!teleported) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ))); - return; - } - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ))); - } - - static int verifyStructures(CommandSourceStack source, String keyRaw) { - ServerLevel level = source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12)); - return 0; - } - String key = keyRaw == null ? "" : keyRaw.trim(); - if (!key.isEmpty()) { - return verifyStructure(source, level, engine, key); - } - Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); - int available = 0; - int disabled = 0; - int suppressed = 0; - int unreachableBiomes = 0; - int unsupported = 0; - for (Identifier identifier : registry.keySet()) { - Optional> holder = registry.get(identifier); - if (holder.isEmpty()) { - continue; - } - NativeStructureAvailability availability = nativeAvailability(source, level, engine, - identifier.toString(), holder.get()); - switch (availability) { - case AVAILABLE -> available++; - case WORLD_DISABLED, FILTERED -> disabled++; - case IRIS_SUPPRESSED -> suppressed++; - case BIOME_UNREACHABLE -> unreachableBiomes++; - case NO_PLACEMENT -> unsupported++; - } - } - int irisPlaced = IrisStructureLocator.placedKeys(engine).size(); - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, MessageArgument.untrusted("available", available), MessageArgument.untrusted("irisPlaced", irisPlaced), MessageArgument.untrusted("disabled", disabled), MessageArgument.untrusted("suppressed", suppressed), MessageArgument.untrusted("unreachableBiomes", unreachableBiomes), MessageArgument.untrusted("unsupported", unsupported))); - return 1; - } - - private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) { - Optional target = resolveNativeStructure(source, level, engine, key); - if (target.isEmpty()) { - if (IrisStructureLocator.isPlaced(engine, key)) { - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, MessageArgument.untrusted("key", key), MessageArgument.untrusted("key2", key))); - return 1; - } - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, MessageArgument.untrusted("key", key))); - return 0; - } - NativeStructureTarget resolved = target.get(); - if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) { - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key()))); - return 1; - } - if (resolved.availability() != NativeStructureAvailability.AVAILABLE) { - fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability())); - return 0; - } - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key()))); - return 1; - } - - private static Optional resolveNativeStructure(CommandSourceStack source, - ServerLevel level, - Engine engine, - String keyRaw) { - Identifier identifier = Identifier.tryParse(keyRaw); - if (identifier == null) { - return Optional.empty(); - } - Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); - Optional> holder = registry.get(identifier); - if (holder.isEmpty()) { - return Optional.empty(); - } - String key = identifier.toString(); - NativeStructureAvailability availability = nativeAvailability(source, level, engine, key, holder.get()); - return Optional.of(new NativeStructureTarget(key, holder.get(), availability)); - } - - private static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level, - Engine engine, String key, - Holder.Reference holder) { - boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures(); - IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); - boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK; - boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS; - ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator(); - boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator - && irisGenerator.isNativeStructureReachable(holder); - boolean hasPlacement = false; - if (worldEnabled && selected && !suppressed && biomeReachable) { - hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty(); - } - return classifyNativeAvailability(worldEnabled, selected, suppressed, biomeReachable, hasPlacement); - } - - static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected, - boolean suppressed, boolean biomeReachable, - boolean hasPlacement) { - if (!worldEnabled) { - return NativeStructureAvailability.WORLD_DISABLED; - } - if (!selected) { - return NativeStructureAvailability.FILTERED; - } - if (suppressed) { - return NativeStructureAvailability.IRIS_SUPPRESSED; - } - if (!biomeReachable) { - return NativeStructureAvailability.BIOME_UNREACHABLE; - } - if (!hasPlacement) { - return NativeStructureAvailability.NO_PLACEMENT; - } - return NativeStructureAvailability.AVAILABLE; - } - - private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) { - return switch (availability) { - case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located."; - case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage( - key, NativeStructureGenerationStatus.DISABLED_BY_PACK); - case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage( - key, NativeStructureGenerationStatus.REPLACED_BY_IRIS); - case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack."; - case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state."; - case AVAILABLE -> "Native structure " + key + " is available."; - }; - } - - private static int gotoPoi(CommandSourceStack source, String typeRaw) { - ServerLevel level = source.getLevel(); - Engine engine = engineFor(level); - if (engine == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13)); - return 0; - } - String type = typeRaw.trim(); - ServerPlayer player = source.getPlayer(); - if (player == null) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, MessageArgument.untrusted("type", type))); - return 0; - } - locate(source, level, engine, player, Locator.poi(type), "POI " + type); - return 1; - } - - private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator locator, String label) { - MinecraftServer server = source.getServer(); - int chunkX = player.blockPosition().getX() >> 4; - int chunkZ = player.blockPosition().getZ() >> 4; - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING, MessageArgument.untrusted("label", label))); - CompletableFuture search; - try { - search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> { - }); - } catch (WrongEngineBroException e) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN)); - return; - } - UUID playerId = player.getUUID(); - CompletableFuture previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search); - if (previous != null && previous != search) { - previous.cancel(true); - } - search.whenComplete((Position2 at, Throwable error) -> completeLocate( - source, level, engine, player, label, server, playerId, search, at, error)); - } - - private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine, - ServerPlayer player, String label, MinecraftServer server, UUID playerId, - CompletableFuture search, Position2 at, Throwable error) { - if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) { - return; - } - Throwable failure = unwrapCompletionFailure(error); - if (failure instanceof CancellationException) { - ACTIVE_LOCATE_REQUESTS.remove(playerId, search); - return; - } - if (failure != null) { - LOGGER.error("Iris locate failed for {}", label, failure); - server.execute(() -> { - if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, MessageArgument.untrusted("failure", failure))); - } - }); - return; - } - if (at == null) { - server.execute(() -> { - if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, MessageArgument.untrusted("label", label))); - } - }); - return; - } - server.execute(() -> { - if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { - teleportToLocateResult(source, level, engine, player, label, at); - } - }); - } - - private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine, - ServerPlayer player, String label, Position2 at) { - int blockX = (at.getX() << 4) + 8; - int blockZ = (at.getZ() << 4) + 8; - try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport"); - IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { - int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2; - boolean teleported = player.teleportTo( - level, - blockX + 0.5D, - blockY, - blockZ + 0.5D, - Set.of(), - player.getYRot(), - player.getXRot(), - false); - if (!teleported) { - fail(source, IrisLanguage.plain( - ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, - MessageArgument.untrusted("label", label), - MessageArgument.trusted("targetX", blockX), - MessageArgument.trusted("clampedY", blockY), - MessageArgument.trusted("targetZ", blockZ))); - return; - } - ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ))); - } catch (GenerationSessionException e) { - fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label))); - } - } - - private static Throwable unwrapCompletionFailure(Throwable error) { - Throwable failure = error; - while ((failure instanceof CompletionException || failure instanceof ExecutionException) - && failure.getCause() != null) { - failure = failure.getCause(); - } - return failure; - } - - private static int seed(CommandSourceStack source) { + static int seed(CommandSourceStack source) { ServerLevel level = source.getLevel(); Engine engine = engineFor(level); if (engine == null) { @@ -1198,7 +235,7 @@ public final class IrisModdedCommands { return 1; } - private static int goldenhash(CommandSourceStack source, int radius, int threads, ModdedGoldenHash.Mode mode) { + static int goldenhash(CommandSourceStack source, int radius, int threads, ModdedGoldenHash.Mode mode) { ServerLevel level = source.getLevel(); Engine engine = engineFor(level); if (engine == null) { @@ -1209,8 +246,8 @@ public final class IrisModdedCommands { return 1; } - private static int download(CommandSourceStack source, String pack, - String branch, boolean forceOverwrite) { + static int download(CommandSourceStack source, String pack, + String branch, boolean forceOverwrite) { boolean defaultOverworld = PackDownloader.isDefaultOverworld(pack); String baseDownloadSource = defaultOverworld ? "beta release" : "branch " + branch; String downloadSource = forceOverwrite @@ -1238,7 +275,7 @@ public final class IrisModdedCommands { return 1; } - private static int metrics(CommandSourceStack source) { + static int metrics(CommandSourceStack source) { ServerLevel level = source.getLevel(); Engine engine = engineFor(level); if (engine == null) { @@ -1257,6 +294,18 @@ public final class IrisModdedCommands { return 1; } + static int verifyStructures(CommandSourceStack source, String keyRaw) { + return ModdedLocateCommands.verifyStructures(source, keyRaw); + } + + static CompletableFuture suggestStructureKeys(CommandContext context, SuggestionsBuilder builder) { + return ModdedCommandSuggestions.suggestStructureKeys(context, builder); + } + + static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) { + ModdedCommandSuggestions.warnTabFailure(suggestion, source, error); + } + static Engine engineFor(ServerLevel level) { ChunkGenerator generator = level.getChunkSource().getGenerator(); if (generator instanceof IrisModdedChunkGenerator irisGenerator) { @@ -1280,107 +329,6 @@ public final class IrisModdedCommands { return count; } - private static CompletableFuture suggestBiomeKeys(CommandContext context, SuggestionsBuilder builder) { - ModdedCommandFeedback.tab(context.getSource()); - try { - Engine engine = engineFor(context.getSource().getLevel()); - if (engine != null) { - return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder); - } - } catch (Throwable ignored) { - } - return builder.buildFuture(); - } - - private static CompletableFuture suggestRegionKeys(CommandContext context, SuggestionsBuilder builder) { - ModdedCommandFeedback.tab(context.getSource()); - try { - Engine engine = engineFor(context.getSource().getLevel()); - if (engine != null) { - return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder); - } - } catch (Throwable ignored) { - } - return builder.buildFuture(); - } - - private static CompletableFuture suggestObjectKeys(CommandContext context, SuggestionsBuilder builder) { - ModdedCommandFeedback.tab(context.getSource()); - try { - Engine engine = engineFor(context.getSource().getLevel()); - if (engine != null) { - return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder); - } - } catch (Throwable ignored) { - } - return builder.buildFuture(); - } - - static CompletableFuture suggestStructureKeys(CommandContext context, SuggestionsBuilder builder) { - ModdedCommandFeedback.tab(context.getSource()); - try { - Engine engine = engineFor(context.getSource().getLevel()); - Collection irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine); - Registry registry = context.getSource().getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); - List nativeKeys = new ArrayList<>(registry.keySet().size()); - for (Identifier identifier : registry.keySet()) { - nativeKeys.add(identifier.toString()); - } - return SharedSuggestionProvider.suggest(combineStructureKeys(irisKeys, nativeKeys), builder); - } catch (Throwable ignored) { - } - return builder.buildFuture(); - } - - static List combineStructureKeys(Collection irisKeys, Collection nativeKeys) { - Set combined = new TreeSet<>(); - combined.addAll(irisKeys); - combined.addAll(nativeKeys); - return List.copyOf(combined); - } - - private static CompletableFuture suggestPackNames(CommandContext context, SuggestionsBuilder builder) { - ModdedCommandFeedback.tab(context.getSource()); - Set names = new TreeSet<>(); - names.add("overworld"); - try { - File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile(); - File[] children = packs.listFiles(); - if (children != null) { - for (File child : children) { - if (!child.isDirectory()) { - continue; - } - String packName = child.getName(); - names.add(packName); - File dimensions = new File(child, "dimensions"); - File[] dimensionFiles = dimensions.listFiles( - (File directory, String name) -> name.endsWith(".json")); - if (dimensionFiles == null) { - continue; - } - for (File dimensionFile : dimensionFiles) { - String fileName = dimensionFile.getName(); - names.add(packName + ":" + fileName.substring(0, fileName.length() - 5)); - } - } - } - } catch (Throwable ignored) { - } - return SharedSuggestionProvider.suggest(names, builder); - } - - private static CompletableFuture suggestDimensionNames(CommandContext context, SuggestionsBuilder builder) { - ModdedCommandFeedback.tab(context.getSource()); - List names = new ArrayList<>(); - for (ServerLevel level : context.getSource().getServer().getAllLevels()) { - if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { - names.add(level.dimension().identifier().toString()); - } - } - return SharedSuggestionProvider.suggest(names, builder); - } - static void ok(CommandSourceStack source, String message) { ModdedCommandFeedback.ok(source, message); } @@ -1392,17 +340,4 @@ public final class IrisModdedCommands { static void fail(CommandSourceStack source, String message) { ModdedCommandFeedback.fail(source, message); } - - enum NativeStructureAvailability { - AVAILABLE, - WORLD_DISABLED, - FILTERED, - IRIS_SUPPRESSED, - BIOME_UNREACHABLE, - NO_PLACEMENT - } - - private record NativeStructureTarget(String key, Holder.Reference holder, - NativeStructureAvailability availability) { - } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java new file mode 100644 index 000000000..6da9f6fa4 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java @@ -0,0 +1,191 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.modded.ModdedEngineBootstrap; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.levelgen.structure.Structure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +final class ModdedCommandSuggestions { + static final SuggestionProvider BIOME_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder); + static final SuggestionProvider REGION_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder); + static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder); + static final SuggestionProvider STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder); + static final SuggestionProvider POI_TYPES = (CommandContext context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder); + static final SuggestionProvider PACK_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestPackNames(context, builder); + static final SuggestionProvider DIMENSION_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder); + + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final int TAB_FAILURE_KEYS_MAX = 256; + private static final Set REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet(); + + private ModdedCommandSuggestions() { + } + + private static CompletableFuture suggestBiomeKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder); + } + } catch (Throwable e) { + warnTabFailure("biome keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + private static CompletableFuture suggestRegionKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder); + } + } catch (Throwable e) { + warnTabFailure("region keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + private static CompletableFuture suggestObjectKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + if (engine != null) { + return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder); + } + } catch (Throwable e) { + warnTabFailure("object keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + static CompletableFuture suggestStructureKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + try { + Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); + Collection irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine); + Registry registry = context.getSource().getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + List nativeKeys = new ArrayList<>(registry.keySet().size()); + for (Identifier identifier : registry.keySet()) { + nativeKeys.add(identifier.toString()); + } + return SharedSuggestionProvider.suggest(combineStructureKeys(irisKeys, nativeKeys), builder); + } catch (Throwable e) { + warnTabFailure("structure keys", context.getSource(), e); + } + return builder.buildFuture(); + } + + static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) { + String origin = tabOrigin(source); + if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) { + return; + } + if (REPORTED_TAB_FAILURES.size() > TAB_FAILURE_KEYS_MAX) { + REPORTED_TAB_FAILURES.clear(); + } + LOGGER.warn("Iris tab-complete for {} in {} failed; suggestions will be empty", suggestion, origin, error); + } + + private static String tabOrigin(CommandSourceStack source) { + if (source == null) { + return ""; + } + try { + return source.getLevel().dimension().identifier().toString(); + } catch (Throwable originFailure) { + return ""; + } + } + + static List combineStructureKeys(Collection irisKeys, Collection nativeKeys) { + Set combined = new TreeSet<>(); + combined.addAll(irisKeys); + combined.addAll(nativeKeys); + return List.copyOf(combined); + } + + private static CompletableFuture suggestPackNames(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + Set names = new TreeSet<>(); + names.add("overworld"); + try { + File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile(); + File[] children = packs.listFiles(); + if (children != null) { + for (File child : children) { + if (!child.isDirectory()) { + continue; + } + String packName = child.getName(); + names.add(packName); + File dimensions = new File(child, "dimensions"); + File[] dimensionFiles = dimensions.listFiles( + (File directory, String name) -> name.endsWith(".json")); + if (dimensionFiles == null) { + continue; + } + for (File dimensionFile : dimensionFiles) { + String fileName = dimensionFile.getName(); + names.add(packName + ":" + fileName.substring(0, fileName.length() - 5)); + } + } + } + } catch (Throwable e) { + warnTabFailure("pack names", context.getSource(), e); + } + return SharedSuggestionProvider.suggest(names, builder); + } + + private static CompletableFuture suggestDimensionNames(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); + List names = new ArrayList<>(); + for (ServerLevel level : context.getSource().getServer().getAllLevels()) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { + names.add(level.dimension().identifier().toString()); + } + } + return SharedSuggestionProvider.suggest(names, builder); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java new file mode 100644 index 000000000..27ac48273 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java @@ -0,0 +1,344 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.resources.Identifier; + +import java.util.function.Predicate; + +final class ModdedCommandTree { + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + + private ModdedCommandTree() { + } + + static LiteralArgumentBuilder rootTree() { + LiteralArgumentBuilder root = Commands.literal("iris"); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")); + root.then(helpTree()); + + root.then(Commands.literal("version") + .executes((CommandContext context) -> IrisModdedCommands.version(context.getSource()))); + + root.then(Commands.literal("info").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), null)) + .then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension"))))); + + root.then(ModdedWhatCommands.tree()); + + root.then(teleportTree("teleport")); + root.then(teleportTree("tp")); + + root.then(Commands.literal("evacuate").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.evacuate(context.getSource(), null)) + .then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> IrisModdedCommands.evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension"))))); + + root.then(Commands.literal("debug").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.debug(context.getSource()))); + + root.then(Commands.literal("reload").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.reload(context.getSource()))); + root.then(Commands.literal("height").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.height(context.getSource()))); + root.then(Commands.literal("worlds").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), null))); + root.then(Commands.literal("accesslist").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.info(context.getSource(), null))); + + root.then(gotoTree("goto")); + root.then(gotoTree("find")); + + root.then(Commands.literal("seed").requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.seed(context.getSource()))); + + root.then(goldenhashTree("goldenhash")); + root.then(goldenhashTree("gold")); + + root.then(downloadTree("download")); + root.then(downloadTree("dl")); + + root.then(metricsTree("metrics")); + root.then(metricsTree("measure")); + + root.then(regenTree("regen")); + root.then(regenTree("rg")); + + root.then(pregenTree("pregen")); + root.then(pregenTree("pregenerate")); + + root.then(Commands.literal("wand").requires(GATE) + .executes((CommandContext context) -> ModdedObjectCommands.giveWand(context.getSource()))); + root.then(Commands.literal("dust").requires(GATE) + .executes((CommandContext context) -> ModdedObjectCommands.giveDust(context.getSource()))); + root.then(Commands.literal("d").requires(GATE) + .executes((CommandContext context) -> ModdedObjectCommands.giveDust(context.getSource()))); + root.then(ModdedObjectCommands.tree("object")); + root.then(ModdedObjectCommands.tree("o")); + root.then(editTree()); + + root.then(createTree("create")); + root.then(createTree("c")); + + root.then(ModdedStudioCommands.tree("studio")); + root.then(ModdedStudioCommands.tree("std")); + root.then(ModdedStudioCommands.tree("s")); + root.then(ModdedPackCommands.tree("pack")); + root.then(ModdedPackCommands.tree("pk")); + root.then(ModdedWorldCommands.tree("world")); + root.then(ModdedWorldCommands.tree("w")); + root.then(ModdedDatapackCommands.tree("datapack")); + root.then(ModdedDatapackCommands.tree("datapacks")); + root.then(ModdedDatapackCommands.tree("dp")); + root.then(ModdedStructureCommands.tree("structure")); + root.then(ModdedStructureCommands.tree("struct")); + root.then(ModdedStructureCommands.tree("str")); + root.then(ModdedDeveloperCommands.tree("developer")); + root.then(ModdedDeveloperCommands.tree("dev")); + + return root; + } + + private static LiteralArgumentBuilder createTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("name", StringArgumentType.word()) + .executes((CommandContext context) -> + ModdedWorldCommands.createWorld( + context.getSource(), + StringArgumentType.getString(context, "name"), + "overworld", + 1337L)) + .then(Commands.argument("pack", StringArgumentType.string()).suggests(ModdedCommandSuggestions.PACK_NAMES) + .executes((CommandContext context) -> ModdedWorldCommands.createWorld(context.getSource(), + StringArgumentType.getString(context, "name"), + StringArgumentType.getString(context, "pack"), + 1337L)) + .then(Commands.argument("seed", LongArgumentType.longArg()) + .executes((CommandContext context) -> ModdedWorldCommands.createWorld(context.getSource(), + StringArgumentType.getString(context, "name"), + StringArgumentType.getString(context, "pack"), + LongArgumentType.getLong(context, "seed")))))); + } + + private static LiteralArgumentBuilder teleportTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> + IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null)) + .then(Commands.argument("player", EntityArgument.player()) + .executes((CommandContext context) -> + IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), + EntityArgument.getPlayer(context, "player"))))); + } + + private static LiteralArgumentBuilder helpTree() { + return Commands.literal("help") + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")) + .then(Commands.argument("section", StringArgumentType.greedyString()) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section")))); + } + + private static LiteralArgumentBuilder downloadTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES) + .executes((CommandContext context) -> + IrisModdedCommands.download(context.getSource(), + StringArgumentType.getString(context, "pack"), "stable", false)) + .then(Commands.literal("force") + .executes((CommandContext context) -> + IrisModdedCommands.download(context.getSource(), + StringArgumentType.getString(context, "pack"), "stable", true))) + .then(Commands.argument("overwrite", BoolArgumentType.bool()) + .executes((CommandContext context) -> + IrisModdedCommands.download(context.getSource(), + StringArgumentType.getString(context, "pack"), "stable", + BoolArgumentType.getBool(context, "overwrite")))) + .then(Commands.argument("branch", StringArgumentType.word()) + .executes((CommandContext context) -> + IrisModdedCommands.download(context.getSource(), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "branch"), false)) + .then(Commands.literal("force") + .executes((CommandContext context) -> + IrisModdedCommands.download(context.getSource(), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "branch"), true))) + .then(Commands.argument("overwrite", BoolArgumentType.bool()) + .executes((CommandContext context) -> + IrisModdedCommands.download(context.getSource(), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "branch"), + BoolArgumentType.getBool(context, "overwrite")))))); + } + + private static LiteralArgumentBuilder metricsTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.metrics(context.getSource())); + } + + private static LiteralArgumentBuilder regenTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.regen(context.getSource(), 0)) + .then(Commands.argument("radius", IntegerArgumentType.integer(0, 64)) + .executes((CommandContext context) -> IrisModdedCommands.regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius")))); + } + + private static LiteralArgumentBuilder gotoTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) + .then(Commands.literal("biome") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("region") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("object") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("structure") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS) + .executes((CommandContext context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("poi") + .then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.POI_TYPES) + .executes((CommandContext context) -> ModdedLocateCommands.gotoPoi(context.getSource(), StringArgumentType.getString(context, "type"))))); + } + + private static LiteralArgumentBuilder pregenTree(String name) { + RequiredArgumentBuilder radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000)) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, false, false, false, false, false)); + attachPregenCenter(radius, false); + attachPregenFlags(radius, false, false, false, false, false); + RequiredArgumentBuilder dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, true, false, false, false, false)); + attachPregenCenter(dimension, true); + attachPregenFlags(dimension, true, false, false, false, false); + radius.then(dimension); + + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) + .then(Commands.literal("start") + .then(radius)) + .then(Commands.literal("stop") + .executes((CommandContext context) -> ModdedPregenCommands.pregenStop(context.getSource()))) + .then(Commands.literal("x") + .executes((CommandContext context) -> ModdedPregenCommands.pregenStop(context.getSource()))) + .then(Commands.literal("pause") + .executes((CommandContext context) -> ModdedPregenCommands.pregenPause(context.getSource()))) + .then(Commands.literal("resume") + .executes((CommandContext context) -> ModdedPregenCommands.pregenPause(context.getSource()))) + .then(Commands.literal("status") + .executes((CommandContext context) -> ModdedPregenCommands.pregenStatus(context.getSource()))); + } + + private static void attachPregenCenter(ArgumentBuilder node, boolean withDimension) { + RequiredArgumentBuilder z = Commands.argument("z", IntegerArgumentType.integer()) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, withDimension, true, false, false, false)); + attachPregenFlags(z, withDimension, true, false, false, false); + node.then(Commands.literal("at") + .then(Commands.argument("x", IntegerArgumentType.integer()) + .then(z))); + } + + private static void attachPregenFlags(ArgumentBuilder node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) { + if (!gui) { + node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache)); + } + if (!sync) { + node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache)); + } + if (!nocache) { + node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true)); + } + } + + private static LiteralArgumentBuilder pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) { + LiteralArgumentBuilder flag = Commands.literal(name) + .executes((CommandContext context) -> ModdedPregenCommands.pregenStart(context, withDimension, withCenter, gui, sync, nocache)); + attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache); + return flag; + } + + private static LiteralArgumentBuilder goldenhashTree(String name) { + LiteralArgumentBuilder radiusAndThreads = Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO)); + attachModes(radiusAndThreads, (CommandContext context) -> 8, (CommandContext context) -> 8); + + com.mojang.brigadier.builder.RequiredArgumentBuilder radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256)) + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO)); + attachModes(radius, (CommandContext context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext context) -> 8); + + com.mojang.brigadier.builder.RequiredArgumentBuilder threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64)) + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO)); + attachModes(threads, (CommandContext context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext context) -> IntegerArgumentType.getInteger(context, "threads")); + + radius.then(threads); + radiusAndThreads.then(radius); + return radiusAndThreads; + } + + private interface IntExtractor { + int extract(CommandContext context); + } + + private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder node, IntExtractor radius, IntExtractor threads) { + node.then(Commands.literal("capture") + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE))); + node.then(Commands.literal("verify") + .executes((CommandContext context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY))); + } + + private static LiteralArgumentBuilder editTree() { + return Commands.literal("edit").requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "edit")) + .then(Commands.literal("biome") + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("b") + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("region") + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("r") + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), null)) + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS) + .executes((CommandContext context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key"))))) + .then(Commands.literal("dimension") + .executes((CommandContext context) -> ModdedEditCommands.editDimension(context.getSource()))) + .then(Commands.literal("d") + .executes((CommandContext context) -> ModdedEditCommands.editDimension(context.getSource()))); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java index b0897ccbd..1ac745d9b 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java @@ -108,7 +108,7 @@ public final class ModdedDustRevealer { pos.immutable(), key, level.getMinY(), - level.getMaxY(), + level.getMaxY() + 1, new AtomicBoolean()); RevealRun previous = ACTIVE_RUNS.put(player.getUUID(), run); if (previous != null) { @@ -144,14 +144,14 @@ public final class ModdedDustRevealer { run.key(), run.engine().getMinHeight(), run.minY(), - run.maxY(), + run.maxYExclusive(), run.cancelled(), (int x, int relativeY, int z) -> run.engine().getObjectPlacementKey(x, relativeY, z)); } static List collect(BlockPos origin, String key, int engineMinY, - int minY, int maxY, AtomicBoolean cancelled, + int minY, int maxYExclusive, AtomicBoolean cancelled, ObjectPlacementLookup lookup) { List hits = new ArrayList<>(); Set visited = new HashSet<>(); @@ -169,7 +169,7 @@ public final class ModdedDustRevealer { } BlockPos next = current.offset(dx, dy, dz); if (next.getY() < minY - || next.getY() >= maxY + || next.getY() >= maxYExclusive || !visited.add(next)) { continue; } @@ -483,7 +483,7 @@ public final class ModdedDustRevealer { BlockPos origin, String key, int minY, - int maxY, + int maxYExclusive, AtomicBoolean cancelled ) { } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java new file mode 100644 index 000000000..fbcfa1876 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java @@ -0,0 +1,133 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.gui.GuiHost; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.File; + +final class ModdedEditCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + + private ModdedEditCommands() { + } + + static int editBiome(CommandSourceStack source, String key) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS)); + return 0; + } + IrisBiome biome; + if (key == null || key.isBlank()) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY)); + return 0; + } + BlockPos pos = player.blockPosition(); + try { + biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ()); + } catch (Throwable e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); + return 0; + } + } else { + biome = engine.getData().getBiomeLoader().load(key.trim()); + if (biome == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, MessageArgument.untrusted("key", key))); + return 0; + } + } + return openJson(source, biome); + } + + static int editRegion(CommandSourceStack source, String key) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2)); + return 0; + } + IrisRegion region; + if (key == null || key.isBlank()) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY)); + return 0; + } + BlockPos pos = player.blockPosition(); + try { + region = engine.getRegion(pos.getX(), pos.getZ()); + } catch (Throwable e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))); + return 0; + } + } else { + region = engine.getData().getRegionLoader().load(key.trim()); + if (region == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION, MessageArgument.untrusted("key", key))); + return 0; + } + } + return openJson(source, region); + } + + static int editDimension(CommandSourceStack source) { + Engine engine = IrisModdedCommands.engineFor(source.getLevel()); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3)); + return 0; + } + return openJson(source, engine.getDimension()); + } + + private static int openJson(CommandSourceStack source, IrisRegistrant registrant) { + if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, MessageArgument.untrusted("value", ModdedGuiHost.guiUnavailableReason()))); + return 0; + } + if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM)); + return 0; + } + File file = registrant.getLoadFile(); + try { + Desktop.getDesktop().open(file); + } catch (Throwable e) { + LOGGER.error("Iris edit failed to open {}", file, e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", registrant.getTypeName()), MessageArgument.untrusted("value2", file.getName()))); + return 1; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java new file mode 100644 index 000000000..2a74c0377 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java @@ -0,0 +1,535 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.GenerationSessionLease; +import art.arcane.iris.engine.framework.IrisStructureLocator; +import art.arcane.iris.engine.framework.Locator; +import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; +import art.arcane.iris.engine.framework.WrongEngineBroException; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.NativeStructureGenerationStatus; +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.Position2; +import com.mojang.datafixers.util.Pair; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Relative; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.Structure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +final class ModdedLocateCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final long LOCATE_TIMEOUT_MS = 120000L; + private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100; + private static final ConcurrentHashMap> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>(); + + private ModdedLocateCommands() { + } + + static int gotoBiome(CommandSourceStack source, String key) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3)); + return 0; + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8)); + return 0; + } + IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim()); + if (biome == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key))); + return 0; + } + locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey()); + return 1; + } + + static int gotoRegion(CommandSourceStack source, String key) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4)); + return 0; + } + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9)); + return 0; + } + IrisRegion region = engine.getData().getRegionLoader().load(key.trim()); + if (region == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key))); + return 0; + } + if (!engine.getDimension().getRegions().contains(region.getLoadKey())) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey()))); + return 0; + } + locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey()); + return 1; + } + + static int gotoObject(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10)); + return 0; + } + String key = keyRaw.trim(); + if (!engine.hasObjectPlacement(key)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length))); + return 0; + } + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length))); + return 0; + } + locate(source, level, engine, player, Locator.object(key), "object " + key); + return 1; + } + + static int gotoStructure(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11)); + return 0; + } + String key = keyRaw.trim(); + if (key.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE)); + return 0; + } + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5)); + return 0; + } + Optional resolved = resolveNativeStructure(source, level, engine, key); + if (resolved.isEmpty()) { + if (IrisStructureLocator.isPlaced(engine, key)) { + locateIrisStructure(source, level, engine, player, key); + return 1; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, MessageArgument.untrusted("key", key))); + return 0; + } + NativeStructureTarget target = resolved.get(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false); + if (!decision.generate() + && decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + IrisModdedCommands.fail(source, NativeStructureGenerationPolicy.generationStatusMessage( + target.key(), decision.status())); + return 0; + } + if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) { + locateIrisStructure(source, level, engine, player, target.key()); + return 1; + } + if (target.availability() != NativeStructureAvailability.AVAILABLE) { + IrisModdedCommands.fail(source, nativeUnavailableMessage(target.key(), target.availability())); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS))); + runNativeStructureLocate(source, level, player, target); + return 1; + } + + private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine, + ServerPlayer player, String key) { + MinecraftServer server = source.getServer(); + int blockX = player.blockPosition().getX(); + int blockZ = player.blockPosition().getZ(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, MessageArgument.untrusted("key", key))); + Thread thread = new Thread(() -> { + try { + IrisStructureLocator.LocateResult result = + IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024); + if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) { + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key)))); + return; + } + if (!result.found()) { + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, MessageArgument.untrusted("key", key)))); + return; + } + int targetX = result.originX(); + int targetY = result.baseY() + 2; + int targetZ = result.originZ(); + server.execute(() -> teleportToStructure(source, level, player, targetX, targetY, targetZ, + "Iris-placed structure " + key)); + } catch (Throwable e) { + LOGGER.error("Iris structure locate failed for {}", key, e); + server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())))); + } + }, "Iris Structure Locator"); + thread.setDaemon(true); + thread.start(); + } + + private static void runNativeStructureLocate(CommandSourceStack source, ServerLevel level, + ServerPlayer player, NativeStructureTarget target) { + MinecraftServer server = source.getServer(); + Runnable locateTask = () -> locateNativeStructure(source, level, player, target); + if (Thread.currentThread() == server.getRunningThread()) { + locateTask.run(); + return; + } + server.execute(locateTask); + } + + private static void locateNativeStructure(CommandSourceStack source, ServerLevel level, + ServerPlayer player, NativeStructureTarget target) { + try { + ChunkGenerator generator = level.getChunkSource().getGenerator(); + Pair> found = generator.findNearestMapStructure( + level, + HolderSet.direct(target.holder()), + player.blockPosition(), + NATIVE_STRUCTURE_LOCATE_RADIUS, + false); + if (found == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS))); + return; + } + BlockPos position = found.getFirst(); + int targetX = position.getX(); + int targetZ = position.getZ(); + level.getChunk(targetX >> 4, targetZ >> 4); + int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, targetX, targetZ) + 1; + int targetY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, surfaceY)); + teleportToStructure(source, level, player, targetX, targetY, targetZ, + "native structure " + target.key()); + } catch (Throwable e) { + LOGGER.error("Native structure locate failed for {}", target.key(), e); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("value2", e.getClass().getSimpleName()))); + } + } + + private static void teleportToStructure(CommandSourceStack source, ServerLevel level, ServerPlayer player, + int targetX, int targetY, int targetZ, String label) { + if (player.hasDisconnected() || player.isRemoved()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED)); + return; + } + if (player.level() != level) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN)); + return; + } + level.getChunk(targetX >> 4, targetZ >> 4); + int clampedY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, targetY)); + boolean teleported = player.teleportTo(level, targetX + 0.5D, clampedY, targetZ + 0.5D, + Set.of(), player.getYRot(), player.getXRot(), false); + if (!teleported) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ))); + } + + static int verifyStructures(CommandSourceStack source, String keyRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12)); + return 0; + } + String key = keyRaw == null ? "" : keyRaw.trim(); + if (!key.isEmpty()) { + return verifyStructure(source, level, engine, key); + } + Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + int available = 0; + int disabled = 0; + int suppressed = 0; + int unreachableBiomes = 0; + int unsupported = 0; + for (Identifier identifier : registry.keySet()) { + Optional> holder = registry.get(identifier); + if (holder.isEmpty()) { + continue; + } + NativeStructureAvailability availability = nativeAvailability(source, level, engine, + identifier.toString(), holder.get()); + switch (availability) { + case AVAILABLE -> available++; + case WORLD_DISABLED, FILTERED -> disabled++; + case IRIS_SUPPRESSED -> suppressed++; + case BIOME_UNREACHABLE -> unreachableBiomes++; + case NO_PLACEMENT -> unsupported++; + } + } + int irisPlaced = IrisStructureLocator.placedKeys(engine).size(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, MessageArgument.untrusted("available", available), MessageArgument.untrusted("irisPlaced", irisPlaced), MessageArgument.untrusted("disabled", disabled), MessageArgument.untrusted("suppressed", suppressed), MessageArgument.untrusted("unreachableBiomes", unreachableBiomes), MessageArgument.untrusted("unsupported", unsupported))); + return 1; + } + + private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) { + Optional target = resolveNativeStructure(source, level, engine, key); + if (target.isEmpty()) { + if (IrisStructureLocator.isPlaced(engine, key)) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, MessageArgument.untrusted("key", key), MessageArgument.untrusted("key2", key))); + return 1; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, MessageArgument.untrusted("key", key))); + return 0; + } + NativeStructureTarget resolved = target.get(); + if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) { + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key()))); + return 1; + } + if (resolved.availability() != NativeStructureAvailability.AVAILABLE) { + IrisModdedCommands.fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability())); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key()))); + return 1; + } + + private static Optional resolveNativeStructure(CommandSourceStack source, + ServerLevel level, + Engine engine, + String keyRaw) { + Identifier identifier = Identifier.tryParse(keyRaw); + if (identifier == null) { + return Optional.empty(); + } + Registry registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + Optional> holder = registry.get(identifier); + if (holder.isEmpty()) { + return Optional.empty(); + } + String key = identifier.toString(); + NativeStructureAvailability availability = nativeAvailability(source, level, engine, key, holder.get()); + return Optional.of(new NativeStructureTarget(key, holder.get(), availability)); + } + + private static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level, + Engine engine, String key, + Holder.Reference holder) { + boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures(); + IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false); + boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK; + boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS; + ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator(); + boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator + && irisGenerator.isNativeStructureReachable(holder); + boolean hasPlacement = false; + if (worldEnabled && selected && !suppressed && biomeReachable) { + hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty(); + } + return classifyNativeAvailability(worldEnabled, selected, suppressed, biomeReachable, hasPlacement); + } + + static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected, + boolean suppressed, boolean biomeReachable, + boolean hasPlacement) { + if (!worldEnabled) { + return NativeStructureAvailability.WORLD_DISABLED; + } + if (!selected) { + return NativeStructureAvailability.FILTERED; + } + if (suppressed) { + return NativeStructureAvailability.IRIS_SUPPRESSED; + } + if (!biomeReachable) { + return NativeStructureAvailability.BIOME_UNREACHABLE; + } + if (!hasPlacement) { + return NativeStructureAvailability.NO_PLACEMENT; + } + return NativeStructureAvailability.AVAILABLE; + } + + private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) { + return switch (availability) { + case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located."; + case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage( + key, NativeStructureGenerationStatus.DISABLED_BY_PACK); + case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage( + key, NativeStructureGenerationStatus.REPLACED_BY_IRIS); + case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack."; + case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state."; + case AVAILABLE -> "Native structure " + key + " is available."; + }; + } + + static int gotoPoi(CommandSourceStack source, String typeRaw) { + ServerLevel level = source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13)); + return 0; + } + String type = typeRaw.trim(); + ServerPlayer player = source.getPlayer(); + if (player == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, MessageArgument.untrusted("type", type))); + return 0; + } + locate(source, level, engine, player, Locator.poi(type), "POI " + type); + return 1; + } + + private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator locator, String label) { + MinecraftServer server = source.getServer(); + int chunkX = player.blockPosition().getX() >> 4; + int chunkZ = player.blockPosition().getZ() >> 4; + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING, MessageArgument.untrusted("label", label))); + CompletableFuture search; + try { + search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> { + }); + } catch (WrongEngineBroException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN)); + return; + } + UUID playerId = player.getUUID(); + CompletableFuture previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search); + if (previous != null && previous != search) { + previous.cancel(true); + } + search.whenComplete((Position2 at, Throwable error) -> completeLocate( + source, level, engine, player, label, server, playerId, search, at, error)); + } + + private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine, + ServerPlayer player, String label, MinecraftServer server, UUID playerId, + CompletableFuture search, Position2 at, Throwable error) { + if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) { + return; + } + Throwable failure = unwrapCompletionFailure(error); + if (failure instanceof CancellationException) { + ACTIVE_LOCATE_REQUESTS.remove(playerId, search); + return; + } + if (failure != null) { + LOGGER.error("Iris locate failed for {}", label, failure); + server.execute(() -> { + if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, MessageArgument.untrusted("failure", failure))); + } + }); + return; + } + if (at == null) { + server.execute(() -> { + if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, MessageArgument.untrusted("label", label))); + } + }); + return; + } + server.execute(() -> { + if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) { + teleportToLocateResult(source, level, engine, player, label, at); + } + }); + } + + private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine, + ServerPlayer player, String label, Position2 at) { + int blockX = (at.getX() << 4) + 8; + int blockZ = (at.getZ() << 4) + 8; + try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport"); + IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { + int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2; + boolean teleported = player.teleportTo( + level, + blockX + 0.5D, + blockY, + blockZ + 0.5D, + Set.of(), + player.getYRot(), + player.getXRot(), + false); + if (!teleported) { + IrisModdedCommands.fail(source, IrisLanguage.plain( + ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, + MessageArgument.untrusted("label", label), + MessageArgument.trusted("targetX", blockX), + MessageArgument.trusted("clampedY", blockY), + MessageArgument.trusted("targetZ", blockZ))); + return; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ))); + } catch (GenerationSessionException e) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label))); + } + } + + private static Throwable unwrapCompletionFailure(Throwable error) { + Throwable failure = error; + while ((failure instanceof CompletionException || failure instanceof ExecutionException) + && failure.getCause() != null) { + failure = failure.getCause(); + } + return failure; + } + + enum NativeStructureAvailability { + AVAILABLE, + WORLD_DISABLED, + FILTERED, + IRIS_SUPPRESSED, + BIOME_UNREACHABLE, + NO_PLACEMENT + } + + private record NativeStructureTarget(String key, Holder.Reference holder, + NativeStructureAvailability availability) { + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java index 1f4f86541..fabc4e889 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java @@ -87,7 +87,8 @@ public final class ModdedObjectCommands { if (engine != null) { return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder); } - } catch (Throwable ignored) { + } catch (Throwable e) { + IrisModdedCommands.warnTabFailure("object keys", context.getSource(), e); } return builder.buildFuture(); }; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java new file mode 100644 index 000000000..5d7d2e71d --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java @@ -0,0 +1,101 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.localization.MessageArgument; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; + +final class ModdedPregenCommands { + private ModdedPregenCommands() { + } + + static int pregenStart(CommandContext context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException { + CommandSourceStack source = context.getSource(); + int radius = IntegerArgumentType.getInteger(context, "radius"); + int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0; + int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0; + ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel(); + Engine engine = IrisModdedCommands.engineFor(level); + if (engine == null) { + if (withDimension) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()))); + } else { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("radius", radius))); + } + return 0; + } + boolean showGui = gui && ModdedGuiHost.isGuiLaunchable(); + if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS)); + return 0; + } + ModdedPregenBossBar.begin(source.getPlayer()); + String guiNote; + if (!gui) { + guiNote = ""; + } else if (showGui) { + guiNote = " A progress map window is opening on the server display."; + } else { + guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")"; + } + String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache)."); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", (radius * 2)), MessageArgument.untrusted("value3", (radius * 2)), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ), MessageArgument.untrusted("modeNote", modeNote), MessageArgument.untrusted("guiNote", guiNote))); + return 1; + } + + static int pregenStop(CommandSourceStack source) { + if (ModdedPregenJob.stop()) { + ModdedPregenBossBar.clear(); + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION)); + return 1; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP)); + return 0; + } + + static int pregenPause(CommandSourceStack source) { + Boolean paused = ModdedPregenJob.pauseResume(); + if (paused == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME)); + return 0; + } + IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, MessageArgument.trusted("value", IrisLanguage.plain(paused.booleanValue() ? RuntimeUiMessages.STATUS_PAUSED_LOWER : RuntimeUiMessages.STATUS_RUNNING_LOWER)))); + return 1; + } + + static int pregenStatus(CommandSourceStack source) { + Component status = ModdedPregenJob.statusComponent(); + if (status == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK)); + return 0; + } + IrisModdedCommands.ok(source, status); + return 1; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java index 1e2332f67..d57a69e29 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java @@ -102,7 +102,8 @@ public final class ModdedStudioCommands { if (engine != null) { return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder); } - } catch (Throwable ignored) { + } catch (Throwable e) { + IrisModdedCommands.warnTabFailure("generator keys", context.getSource(), e); } return builder.buildFuture(); }; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java index 4530475cc..fe9e32445 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java @@ -20,6 +20,7 @@ package art.arcane.iris.modded.command; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.pack.BrokenPackException; +import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.MainWorldService; @@ -303,7 +304,14 @@ public final class ModdedWorldCommands { } } catch (Throwable e) { LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e); - IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack))); + if (PackValidationRegistry.get(pack) == null) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack))); + return 0; + } + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_FAILED, + MessageArgument.untrusted("value", pack + ":" + packDimension), + MessageArgument.trusted("value2", e.getClass().getSimpleName() + IrisLanguage.errorDetail(e)))); + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack))); return 0; } ModdedModConfig.setMainWorld(packRef, seed); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCapture.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCapture.java deleted file mode 100644 index 238be29bf..000000000 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCapture.java +++ /dev/null @@ -1,620 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.modded.structure; - -import art.arcane.iris.core.structure.authoring.StructureBackend; -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureKey; -import art.arcane.iris.core.structure.authoring.StructureLoss; -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; -import art.arcane.iris.core.structure.authoring.StructureSource; -import art.arcane.iris.engine.object.IrisObject; -import art.arcane.iris.engine.object.TileData; -import art.arcane.iris.util.common.math.IrisBlockVector; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.mojang.datafixers.util.Pair; -import net.minecraft.SharedConstants; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.Registry; -import net.minecraft.core.Vec3i; -import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; -import net.minecraft.nbt.NbtUtils; -import net.minecraft.nbt.Tag; -import net.minecraft.resources.Identifier; -import net.minecraft.resources.RegistryOps; -import net.minecraft.resources.ResourceKey; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.RandomSource; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.JigsawBlock; -import net.minecraft.world.level.block.Rotation; -import net.minecraft.world.level.levelgen.structure.Structure; -import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; -import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; -import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement; -import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; -import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; -import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding; -import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -final class ModdedJigsawStructureCapture { - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - private final MinecraftServer server; - private final StructureKey sourceKey; - private final StructureKey targetKey; - private final StructureSource.Kind sourceKind; - private final Registry structureRegistry; - private final Registry poolRegistry; - private final Registry blockRegistry; - private final StructureTemplateManager templateManager; - private final RegistryOps registryOps; - private final Map, ResourceKey> aliases; - private final Map objects; - private final Map> pieces; - private final Map> pools; - private final EnumSet capabilities; - private final List losses; - private final Set recordedLosses; - private final Set visitedPools; - private final Deque pendingPools; - private int blocks; - - private ModdedJigsawStructureCapture( - MinecraftServer server, - StructureKey sourceKey, - StructureKey targetKey, - StructureSource.Kind sourceKind - ) { - this.server = Objects.requireNonNull(server); - this.sourceKey = Objects.requireNonNull(sourceKey); - this.targetKey = Objects.requireNonNull(targetKey); - this.sourceKind = Objects.requireNonNull(sourceKind); - structureRegistry = server.registryAccess().lookupOrThrow(Registries.STRUCTURE); - poolRegistry = server.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL); - blockRegistry = server.registryAccess().lookupOrThrow(Registries.BLOCK); - templateManager = server.getStructureManager(); - registryOps = RegistryOps.create(NbtOps.INSTANCE, server.registryAccess()); - aliases = new HashMap<>(); - objects = new LinkedHashMap<>(); - pieces = new LinkedHashMap<>(); - pools = new LinkedHashMap<>(); - capabilities = EnumSet.of( - StructureCapability.BLOCKS, - StructureCapability.CONNECTORS, - StructureCapability.IRIS_PLACEMENT - ); - losses = new ArrayList<>(); - recordedLosses = new HashSet<>(); - visitedPools = new HashSet<>(); - pendingPools = new ArrayDeque<>(); - } - - static Capture capture( - MinecraftServer server, - StructureKey sourceKey, - StructureKey targetKey, - StructureSource.Kind sourceKind - ) throws IOException { - return new ModdedJigsawStructureCapture(server, sourceKey, targetKey, sourceKind).capture(); - } - - private Capture capture() throws IOException { - Identifier sourceIdentifier = identifier(sourceKey); - Structure structure = structureRegistry.getValue(sourceIdentifier); - if (structure == null) { - throw new IllegalArgumentException("No registered structure exists for " + sourceKey); - } - if (!(structure instanceof JigsawStructure jigsaw)) { - throw new UnsupportedStructureTypeException("Structure " + sourceKey + " uses " - + structure.getClass().getSimpleName() + "; only jigsaw structure graphs can be converted to Iris assembly resources"); - } - - CompoundTag encodedStructure = encodeStructure(structure); - configureAliases(jigsaw); - recordRootLosses(jigsaw); - String startPoolKey = poolKey(jigsaw.getStartPool().value()); - pendingPools.add(startPoolKey); - while (!pendingPools.isEmpty()) { - capturePool(pendingPools.removeFirst()); - } - if (pieces.isEmpty()) { - throw new IllegalStateException("Structure " + sourceKey + " produced no importable pieces"); - } - - int maxDepth = Math.max(1, encodedStructure.getIntOr("size", 1)); - int maxDistance = readHorizontalDistance(encodedStructure); - StructureSource source = StructureSource.identified( - sourceKind, - sourceKey, - SharedConstants.getCurrentVersion().name(), - NbtUtils.structureToSnbt(encodedStructure).getBytes(StandardCharsets.UTF_8) - ); - StructureResourceBundle bundle = buildBundle(source, startPoolKey, maxDepth, maxDistance); - return new Capture(bundle, blocks, pieces.size(), pools.size()); - } - - private CompoundTag encodeStructure(Structure structure) { - Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, structure).getOrThrow(); - if (!(encoded instanceof CompoundTag compound)) { - throw new IllegalStateException("Structure codec did not produce a compound for " + sourceKey); - } - return compound; - } - - private void configureAliases(JigsawStructure structure) { - List bindings = structure.getPoolAliases(); - if (bindings.isEmpty()) { - return; - } - RandomSource random = RandomSource.create(stableSeed(sourceKey.value())); - for (PoolAliasBinding binding : bindings) { - binding.forEachResolved(random, aliases::put); - } - addLossOnce( - "pool_aliases_resolved_once", - StructureLoss.warning( - StructureCapability.CONNECTORS, - "pool_aliases_resolved_once", - bindings.size() + " native pool alias binding(s) were resolved deterministically for the imported graph; per-placement alias variation is not represented." - ) - ); - } - - private void recordRootLosses(JigsawStructure structure) { - losses.add(StructureLoss.warning( - StructureCapability.NATIVE_PLACEMENT, - "native_placement_settings_not_imported", - "Native start height, heightmap projection, expansion, padding, and placement-set settings are not represented by Iris assembly placement." - )); - losses.add(StructureLoss.warning( - StructureCapability.LIQUID_SETTINGS, - "native_liquid_settings_not_imported", - "Native structure liquid placement behavior is not represented beyond the captured block and waterlogged states." - )); - if (structure.terrainAdaptation() != TerrainAdjustment.NONE) { - losses.add(StructureLoss.warning( - StructureCapability.TERRAIN_ADAPTATION, - "terrain_adaptation_not_imported", - "Native terrain adaptation '" + structure.terrainAdaptation().getSerializedName() - + "' is not represented by the Iris assembly." - )); - } - } - - private void capturePool(String sourcePoolKey) throws IOException { - if (!visitedPools.add(sourcePoolKey)) { - return; - } - Identifier sourcePoolIdentifier = Identifier.tryParse(sourcePoolKey); - StructureTemplatePool pool = sourcePoolIdentifier == null ? null : poolRegistry.getValue(sourcePoolIdentifier); - if (pool == null) { - throw new IllegalStateException("Jigsaw graph references missing template pool " + sourcePoolKey); - } - String irisPoolName = poolName(targetKey.path(), sourcePoolKey); - List entries = new ArrayList<>(); - List> templates = pool.getTemplates(); - for (int index = 0; index < templates.size(); index++) { - Pair weighted = templates.get(index); - StructurePoolElement element = weighted.getFirst(); - int weight = Math.max(1, weighted.getSecond()); - Map entry = new LinkedHashMap<>(); - if (element == EmptyPoolElement.INSTANCE) { - entry.put("empty", true); - } else { - entry.put("piece", captureElement(sourcePoolKey, index, element)); - } - entry.put("weight", weight); - entries.add(entry); - } - - Map poolJson = new LinkedHashMap<>(); - poolJson.put("pieces", entries); - String fallback = resolvedPoolKey(pool.getFallback().value()); - if (!fallback.equals(sourcePoolKey)) { - poolJson.put("fallback", poolName(targetKey.path(), fallback)); - pendingPools.addLast(fallback); - } - pools.put(irisPoolName, poolJson); - } - - private String captureElement(String sourcePoolKey, int index, StructurePoolElement element) throws IOException { - Objects.requireNonNull(element, "pool element"); - if (element instanceof SinglePoolElement single) { - return captureSingle(single); - } - String generatedName = generatedPieceName(targetKey.path(), sourcePoolKey, index, element); - if (pieces.containsKey(generatedName)) { - return generatedName; - } - if (element instanceof ListPoolElement list) { - capabilities.add(StructureCapability.LIST_ELEMENTS); - CompositeCapture composite = captureList(list, generatedName); - blocks += composite.object().getBlocks().size(); - emitPiece(generatedName, composite.object(), composite.losses(), element); - return generatedName; - } - IrisObject object = emptyObject(element); - StructureCapability unsupported = element instanceof FeaturePoolElement - ? StructureCapability.FEATURE_ELEMENTS : StructureCapability.BLOCKS; - StructureLoss loss = StructureLoss.warning( - unsupported, - "unsupported_pool_element", - "Pool element " + element.getClass().getSimpleName() + " was represented as an empty Iris piece." - ); - emitPiece(generatedName, object, List.of(loss), element); - return generatedName; - } - - private String captureSingle(SinglePoolElement element) throws IOException { - Identifier templateIdentifier = element.getTemplateLocation(); - StructureKey templateKey = StructureKey.parse(templateIdentifier.toString()); - boolean legacy = element instanceof LegacySinglePoolElement; - String pieceName = legacy - ? legacyPieceName(targetKey.path(), templateKey.value()) - : pieceName(targetKey.path(), templateKey.value()); - if (pieces.containsKey(pieceName)) { - return pieceName; - } - StructureTemplate template = templateManager.get(templateIdentifier) - .orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier)); - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture( - templateKey, template, blockRegistry, true, !legacy); - capabilities.addAll(capture.capabilities()); - blocks += capture.blocks(); - List pieceLosses = new ArrayList<>(capture.losses()); - pieceLosses.addAll(elementLosses(element)); - emitPiece(pieceName, capture.object(), pieceLosses, element); - return pieceName; - } - - private CompositeCapture captureList(ListPoolElement list, String pieceName) throws IOException { - Vec3i size = list.getSize(templateManager, Rotation.NONE); - IrisObject composite = new IrisObject( - Math.max(1, size.getX()), - Math.max(1, size.getY()), - Math.max(1, size.getZ()) - ); - List compositeLosses = new ArrayList<>(); - for (StructurePoolElement child : list.getElements()) { - if (child == EmptyPoolElement.INSTANCE) { - continue; - } - if (child instanceof SinglePoolElement single) { - Identifier templateIdentifier = single.getTemplateLocation(); - StructureTemplate template = templateManager.get(templateIdentifier) - .orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier)); - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture( - StructureKey.parse(templateIdentifier.toString()), - template, - blockRegistry, - true, - !(single instanceof LegacySinglePoolElement) - ); - merge(composite, capture.object()); - capabilities.addAll(capture.capabilities()); - compositeLosses.addAll(capture.losses()); - compositeLosses.addAll(elementLosses(single)); - continue; - } - if (child instanceof ListPoolElement nested) { - CompositeCapture nestedCapture = captureList(nested, pieceName); - merge(composite, nestedCapture.object()); - compositeLosses.addAll(nestedCapture.losses()); - continue; - } - StructureCapability unsupported = child instanceof FeaturePoolElement - ? StructureCapability.FEATURE_ELEMENTS : StructureCapability.LIST_ELEMENTS; - compositeLosses.add(StructureLoss.warning( - unsupported, - "list_child_not_imported", - "List element child " + child.getClass().getSimpleName() + " could not be flattened into the Iris object." - )); - } - compositeLosses.addAll(elementLosses(list)); - return new CompositeCapture(composite, compositeLosses); - } - - private List elementLosses(StructurePoolElement element) { - List elementLosses = new ArrayList<>(); - if (element.getProjection() != StructureTemplatePool.Projection.RIGID) { - elementLosses.add(StructureLoss.warning( - StructureCapability.PROJECTION, - "terrain_matching_projection_not_imported", - "Pool projection '" + element.getProjection().getSerializedName() - + "' was converted to rigid Iris piece placement." - )); - } - Tag encoded = StructurePoolElement.CODEC.encodeStart(registryOps, element).getOrThrow(); - if (encoded instanceof CompoundTag compound) { - String processors = compound.getStringOr("processors", ""); - if (!processors.isEmpty() && !processors.equals("minecraft:empty")) { - elementLosses.add(StructureLoss.warning( - StructureCapability.PROCESSORS, - "native_processors_not_imported", - "Native processor list '" + processors + "' was not applied to the captured template." - )); - } - if (compound.contains("override_liquid_settings")) { - elementLosses.add(StructureLoss.warning( - StructureCapability.LIQUID_SETTINGS, - "element_liquid_settings_not_imported", - "The pool element's liquid setting override is not represented by Iris placement." - )); - } - } - return elementLosses; - } - - private void emitPiece( - String pieceName, - IrisObject object, - List pieceLosses, - StructurePoolElement element - ) throws IOException { - String objectResource = "objects/" + pieceName + ".iob"; - objects.put(pieceName, serialize(object)); - for (StructureLoss loss : pieceLosses) { - losses.add(loss.affecting(objectResource)); - } - List> connectors = connectors(element, pieceName); - Map pieceJson = new LinkedHashMap<>(); - pieceJson.put("object", pieceName); - pieceJson.put("connectors", connectors); - pieceJson.put("rotatable", true); - pieces.put(pieceName, pieceJson); - } - - private List> connectors(StructurePoolElement element, String pieceName) { - List sourceConnectors = element.getShuffledJigsawBlocks( - templateManager, - BlockPos.ZERO, - Rotation.NONE, - RandomSource.create(stableSeed(sourceKey.value() + ":" + pieceName)) - ); - List> connectors = new ArrayList<>(sourceConnectors.size()); - for (StructureTemplate.JigsawBlockInfo source : sourceConnectors) { - recordConnectorLosses(source, pieceName); - ResourceKey resolvedPool = aliases.getOrDefault(source.pool(), source.pool()); - String sourcePoolKey = resolvedPool.identifier().toString(); - pendingPools.addLast(sourcePoolKey); - Map position = new LinkedHashMap<>(); - position.put("x", source.info().pos().getX()); - position.put("y", source.info().pos().getY()); - position.put("z", source.info().pos().getZ()); - Map connector = new LinkedHashMap<>(); - connector.put("position", position); - connector.put("direction", directionName(JigsawBlock.getFrontFacing(source.info().state()))); - connector.put("top", directionName(JigsawBlock.getTopFacing(source.info().state()))); - connector.put("pool", poolName(targetKey.path(), sourcePoolKey)); - connector.put("name", source.name().toString()); - connector.put("targetName", source.target().toString()); - connector.put("joint", source.jointType().getSerializedName().equals("aligned") ? "ALIGNED" : "ROLLABLE"); - connectors.add(connector); - } - return connectors; - } - - private void recordConnectorLosses(StructureTemplate.JigsawBlockInfo connector, String pieceName) { - String pieceResource = "jigsaw-pieces/" + pieceName + ".json"; - if (connector.placementPriority() != 0 || connector.selectionPriority() != 0) { - addLossOnce( - "connector-priority:" + pieceName, - StructureLoss.warning( - StructureCapability.CONNECTORS, - "connector_priorities_not_imported", - "Native connector placement and selection priorities are not represented by Iris assembly ordering." - ).affecting(pieceResource) - ); - } - } - - private StructureResourceBundle buildBundle( - StructureSource source, - String startPoolKey, - int maxDepth, - int maxDistance - ) { - StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(targetKey) - .source(source) - .backend(StructureBackend.IRIS_ASSEMBLY) - .capabilities(capabilities) - .losses(losses); - for (Map.Entry entry : objects.entrySet()) { - bundle.resource("objects/" + entry.getKey() + ".iob", entry.getValue()); - } - for (Map.Entry> entry : pieces.entrySet()) { - bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue())); - } - for (Map.Entry> entry : pools.entrySet()) { - bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue())); - } - bundle.textResource( - "structures/" + targetKey.path() + ".json", - GSON.toJson(structureJson(sourceKey.value(), poolName(targetKey.path(), startPoolKey), maxDepth, maxDistance)) - ); - return bundle.build(); - } - - private String resolvedPoolKey(StructureTemplatePool pool) { - ResourceKey raw = poolResourceKey(pool); - return aliases.getOrDefault(raw, raw).identifier().toString(); - } - - private String poolKey(StructureTemplatePool pool) { - return poolResourceKey(pool).identifier().toString(); - } - - private ResourceKey poolResourceKey(StructureTemplatePool pool) { - Identifier identifier = poolRegistry.getKey(pool); - if (identifier == null) { - throw new IllegalStateException("Jigsaw structure references an unregistered template pool"); - } - return ResourceKey.create(Registries.TEMPLATE_POOL, identifier); - } - - private void addLossOnce(String key, StructureLoss loss) { - if (recordedLosses.add(key)) { - losses.add(loss); - } - } - - private static void merge(IrisObject target, IrisObject source) { - for (IrisBlockVector position : source.getBlocks().keys()) { - int x = position.getBlockX() + source.getCenter().getX(); - int y = position.getBlockY() + source.getCenter().getY(); - int z = position.getBlockZ() + source.getCenter().getZ(); - if (x < 0 || y < 0 || z < 0 || x >= target.getW() || y >= target.getH() || z >= target.getD()) { - continue; - } - target.setUnsignedTile(x, y, z, null); - target.setUnsigned(x, y, z, source.getBlocks().get(position)); - TileData tile = source.getStates().get(position); - if (tile != null) { - target.setUnsignedTile(x, y, z, tile); - } - } - } - - private static IrisObject emptyObject() { - return new IrisObject(1, 1, 1); - } - - private IrisObject emptyObject(StructurePoolElement element) { - Vec3i size = element.getSize(templateManager, Rotation.NONE); - return new IrisObject( - Math.max(1, size.getX()), - Math.max(1, size.getY()), - Math.max(1, size.getZ()) - ); - } - - private static byte[] serialize(IrisObject object) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - object.write(output); - return output.toByteArray(); - } - - private static int readHorizontalDistance(CompoundTag encodedStructure) { - int scalar = encodedStructure.getIntOr("max_distance_from_center", -1); - if (scalar > 0) { - return scalar; - } - CompoundTag compound = encodedStructure.getCompoundOrEmpty("max_distance_from_center"); - return Math.max(1, compound.getIntOr("horizontal", 80)); - } - - static Map structureJson(String source, String startPool, int maxDepth, int maxDistance) { - Map root = new LinkedHashMap<>(); - root.put("startPool", startPool); - root.put("maxDepth", Math.max(1, Math.min(30, maxDepth))); - root.put("maxSizeChunks", Math.max(1, Math.min(32, (Math.max(1, maxDistance) + 15) / 16))); - root.put("placeMode", "STRUCTURE_PIECE"); - root.put("vanillaSource", source); - return root; - } - - static String poolName(String base, String sourcePoolKey) { - StructureKey key = StructureKey.parse(sourcePoolKey); - return base + "/pool/" + key.namespace() + "/" + key.path(); - } - - static String pieceName(String base, String templateKey) { - StructureKey key = StructureKey.parse(templateKey); - return base + "/piece/" + key.namespace() + "/" + key.path(); - } - - static String legacyPieceName(String base, String templateKey) { - StructureKey key = StructureKey.parse(templateKey); - return base + "/piece/generated/legacy/" + key.namespace() + "/" + key.path(); - } - - static String directionName(Direction direction) { - return switch (direction) { - case UP -> "UP_POSITIVE_Y"; - case DOWN -> "DOWN_NEGATIVE_Y"; - case SOUTH -> "SOUTH_POSITIVE_Z"; - case EAST -> "EAST_POSITIVE_X"; - case WEST -> "WEST_NEGATIVE_X"; - case NORTH -> "NORTH_NEGATIVE_Z"; - }; - } - - private static String generatedPieceName(String base, String sourcePoolKey, int index, StructurePoolElement element) { - StructureKey poolKey = StructureKey.parse(sourcePoolKey); - String type = element instanceof ListPoolElement ? "list" : element instanceof FeaturePoolElement ? "feature" : "unsupported"; - return base + "/piece/generated/" + type + "/" + poolKey.namespace() + "/" + poolKey.path() + "/" + index; - } - - private static long stableSeed(String value) { - long hash = 0xcbf29ce484222325L; - for (int index = 0; index < value.length(); index++) { - hash ^= value.charAt(index); - hash *= 0x100000001b3L; - } - return hash; - } - - private static Identifier identifier(StructureKey key) { - return Identifier.fromNamespaceAndPath(key.namespace(), key.path()); - } - - record Capture(StructureResourceBundle bundle, int blocks, int pieces, int pools) { - Capture { - Objects.requireNonNull(bundle); - } - } - - static final class UnsupportedStructureTypeException extends IllegalArgumentException { - UnsupportedStructureTypeException(String message) { - super(message); - } - } - - private record CompositeCapture(IrisObject object, List losses) { - CompositeCapture { - Objects.requireNonNull(object); - losses = List.copyOf(losses); - } - } -} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureImportService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureImportService.java deleted file mode 100644 index 7e217c4e8..000000000 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureImportService.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.modded.structure; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory; -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureKey; -import art.arcane.iris.core.structure.authoring.StructureLoss; -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; -import art.arcane.iris.core.structure.authoring.StructureSource; -import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; -import art.arcane.iris.core.structure.authoring.StructureWriteMode; -import art.arcane.iris.core.structure.authoring.StructureWriteOptions; -import art.arcane.iris.core.structure.authoring.StructureWriteResult; -import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler; -import net.minecraft.SharedConstants; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.NbtUtils; -import net.minecraft.resources.Identifier; -import net.minecraft.server.MinecraftServer; -import net.minecraft.world.level.levelgen.structure.Structure; -import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; - -public final class ModdedStructureImportService { - private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - - private final Supplier server; - - public ModdedStructureImportService(Supplier server) { - this.server = Objects.requireNonNull(server); - } - - public List templateKeys() throws StructureImportException { - try { - MinecraftServer activeServer = requireServerThread(); - return activeServer.getStructureManager().listTemplates() - .map((Identifier identifier) -> StructureKey.parse(identifier.toString())) - .sorted() - .toList(); - } catch (RuntimeException failure) { - throw report("Failed to list native structure templates", failure); - } - } - - public List jigsawStructureKeys() throws StructureImportException { - try { - MinecraftServer activeServer = requireServerThread(); - Registry registry = activeServer.registryAccess().lookupOrThrow(Registries.STRUCTURE); - List keys = new ArrayList<>(); - for (Identifier identifier : registry.keySet()) { - if (registry.getValue(identifier) instanceof JigsawStructure) { - keys.add(StructureKey.parse(identifier.toString())); - } - } - keys.sort(Comparator.naturalOrder()); - return List.copyOf(keys); - } catch (RuntimeException failure) { - throw report("Failed to list registered jigsaw structures", failure); - } - } - - public PreparedImport prepareTemplate(TemplateImportOptions options) throws StructureImportException { - Objects.requireNonNull(options); - try { - MinecraftServer activeServer = requireServerThread(); - Identifier sourceIdentifier = identifier(options.sourceKey()); - StructureTemplate template = activeServer.getStructureManager().get(sourceIdentifier) - .orElseThrow(() -> new IllegalArgumentException("No structure template exists for " + options.sourceKey())); - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture( - options.sourceKey(), - template, - activeServer.registryAccess().lookupOrThrow(Registries.BLOCK), - false - ); - StructureSource source = StructureSource.identified( - options.sourceKind(), - options.sourceKey(), - SharedConstants.getCurrentVersion().name(), - NbtUtils.structureToSnbt(capture.sourceTag()).getBytes(StandardCharsets.UTF_8) - ); - IrisStructureBundleFactory.SinglePieceOptions bundleOptions = new IrisStructureBundleFactory.SinglePieceOptions( - options.targetKey(), - source, - options.targetKey().path(), - capture.object(), - Math.max(capture.width(), capture.depth()), - options.placeMode(), - options.objectOnly(), - capture.capabilities(), - capture.losses() - ); - StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(bundleOptions); - if (!options.objectOnly()) { - StructureResourceBundleGraphCompiler.requireViable(bundle); - } - return new PreparedImport( - options.packRoot(), - options.writeOptions(), - ImportKind.TEMPLATE, - bundle, - capture.blocks(), - 1, - options.objectOnly() ? 0 : 1 - ); - } catch (IOException | RuntimeException failure) { - throw report("Failed to prepare structure template import " + options.sourceKey(), failure); - } - } - - public PreparedImport prepareJigsawStructure(JigsawImportOptions options) throws StructureImportException { - Objects.requireNonNull(options); - try { - MinecraftServer activeServer = requireServerThread(); - ModdedJigsawStructureCapture.Capture capture = ModdedJigsawStructureCapture.capture( - activeServer, - options.sourceKey(), - options.targetKey(), - options.sourceKind() - ); - StructureResourceBundleGraphCompiler.requireViable(capture.bundle()); - return new PreparedImport( - options.packRoot(), - options.writeOptions(), - ImportKind.JIGSAW_STRUCTURE, - capture.bundle(), - capture.blocks(), - capture.pieces(), - capture.pools() - ); - } catch (ModdedJigsawStructureCapture.UnsupportedStructureTypeException failure) { - StructureLoss loss = StructureLoss.error( - StructureCapability.IRIS_PLACEMENT, - "unsupported_native_structure_type", - failure.getMessage() - ); - throw report("Cannot import native structure graph " + options.sourceKey(), failure, List.of(loss)); - } catch (IOException | RuntimeException failure) { - throw report("Failed to prepare jigsaw structure import " + options.sourceKey(), failure); - } - } - - public ImportResult write(PreparedImport prepared) { - Objects.requireNonNull(prepared); - StructureWriteResult writeResult = new StructureTransactionWriter(prepared.packRoot()) - .write(prepared.bundle(), prepared.writeOptions()); - writeResult.failure().ifPresent((Throwable failure) -> LOGGER.error( - "Failed to commit modded structure import {} to {}", - prepared.bundle().key(), - prepared.packRoot(), - failure - )); - if (writeResult.committed()) { - IrisData.getLoaded(prepared.packRoot().toFile()).ifPresent(IrisData::invalidateStructureResources); - } - String message = writeMessage(prepared, writeResult); - return new ImportResult( - writeResult.successful(), - message, - prepared.kind(), - prepared.bundle().key(), - prepared.blocks(), - prepared.pieces(), - prepared.pools(), - prepared.bundle().capabilities(), - prepared.bundle().losses(), - Optional.of(writeResult) - ); - } - - public ImportResult importTemplate(TemplateImportOptions options) { - try { - return write(prepareTemplate(options)); - } catch (StructureImportException failure) { - return failed(options.targetKey(), ImportKind.TEMPLATE, failure.getMessage(), failure.losses()); - } - } - - public ImportResult importJigsawStructure(JigsawImportOptions options) { - try { - return write(prepareJigsawStructure(options)); - } catch (StructureImportException failure) { - return failed(options.targetKey(), ImportKind.JIGSAW_STRUCTURE, failure.getMessage(), failure.losses()); - } - } - - private MinecraftServer requireServerThread() { - MinecraftServer activeServer = server.get(); - if (activeServer == null) { - throw new IllegalStateException("Minecraft server is not available"); - } - if (!activeServer.isSameThread()) { - throw new IllegalStateException("Structure registry capture must run on the logical server thread"); - } - return activeServer; - } - - private StructureImportException report(String context, Exception failure) { - return report(context, failure, List.of()); - } - - private StructureImportException report(String context, Exception failure, List losses) { - LOGGER.error(context, failure); - return new StructureImportException(context + ": " + failureDetail(failure), failure, losses); - } - - private static ImportResult failed( - StructureKey targetKey, - ImportKind kind, - String message, - List losses - ) { - return new ImportResult( - false, - message, - kind, - targetKey, - 0, - 0, - 0, - Set.of(), - losses, - Optional.empty() - ); - } - - private static String writeMessage(PreparedImport prepared, StructureWriteResult result) { - if (result.successful()) { - return switch (result.status()) { - case DRY_RUN -> "Validated import of '" + prepared.bundle().key() + "' without writing files"; - case ADDED -> "Imported '" + prepared.bundle().key() + "'"; - case OVERWRITTEN -> "Overwrote owned import '" + prepared.bundle().key() + "'"; - case UNCHANGED -> "Import '" + prepared.bundle().key() + "' is already current"; - case COMMITTED_CLEANUP_REQUIRED -> "Imported '" + prepared.bundle().key() - + "'; obsolete staging cleanup is still required"; - default -> "Imported '" + prepared.bundle().key() + "'"; - }; - } - if (!result.conflicts().isEmpty()) { - StructureWriteResult.Conflict conflict = result.conflicts().getFirst(); - return "Import conflict for '" + prepared.bundle().key() + "': " + conflict.relativePath() - + " is " + conflict.reason().name().toLowerCase() + "; existing files were preserved"; - } - return "Import failed for '" + prepared.bundle().key() + "': " - + result.failure().map(ModdedStructureImportService::failureDetail).orElse(result.status().name()); - } - - private static String failureDetail(Throwable failure) { - String message = failure.getMessage(); - return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message; - } - - private static Identifier identifier(StructureKey key) { - return Identifier.fromNamespaceAndPath(key.namespace(), key.path()); - } - - public enum ImportKind { - TEMPLATE, - JIGSAW_STRUCTURE - } - - public record TemplateImportOptions( - Path packRoot, - StructureKey sourceKey, - StructureKey targetKey, - StructureSource.Kind sourceKind, - StructureWriteOptions writeOptions, - boolean objectOnly, - String placeMode - ) { - public TemplateImportOptions { - packRoot = normalizedRoot(packRoot); - Objects.requireNonNull(sourceKey); - Objects.requireNonNull(targetKey); - Objects.requireNonNull(sourceKind); - Objects.requireNonNull(writeOptions); - Objects.requireNonNull(placeMode); - if (placeMode.isBlank()) { - throw new IllegalArgumentException("Structure place mode cannot be blank"); - } - } - - public static TemplateImportOptions create( - Path packRoot, - StructureKey sourceKey, - StructureKey targetKey, - StructureWriteMode mode - ) { - return new TemplateImportOptions( - packRoot, - sourceKey, - targetKey, - inferredSourceKind(sourceKey), - new StructureWriteOptions(mode, false), - false, - "CENTER_HEIGHT" - ); - } - } - - public record JigsawImportOptions( - Path packRoot, - StructureKey sourceKey, - StructureKey targetKey, - StructureSource.Kind sourceKind, - StructureWriteOptions writeOptions - ) { - public JigsawImportOptions { - packRoot = normalizedRoot(packRoot); - Objects.requireNonNull(sourceKey); - Objects.requireNonNull(targetKey); - Objects.requireNonNull(sourceKind); - Objects.requireNonNull(writeOptions); - } - - public static JigsawImportOptions create( - Path packRoot, - StructureKey sourceKey, - StructureKey targetKey, - StructureWriteMode mode - ) { - return new JigsawImportOptions( - packRoot, - sourceKey, - targetKey, - inferredSourceKind(sourceKey), - new StructureWriteOptions(mode, false) - ); - } - } - - public record PreparedImport( - Path packRoot, - StructureWriteOptions writeOptions, - ImportKind kind, - StructureResourceBundle bundle, - int blocks, - int pieces, - int pools - ) { - public PreparedImport { - packRoot = normalizedRoot(packRoot); - Objects.requireNonNull(writeOptions); - Objects.requireNonNull(kind); - Objects.requireNonNull(bundle); - if (blocks < 0 || pieces < 0 || pools < 0) { - throw new IllegalArgumentException("Prepared import counts cannot be negative"); - } - } - } - - public record ImportResult( - boolean success, - String message, - ImportKind kind, - StructureKey targetKey, - int blocks, - int pieces, - int pools, - Set capabilities, - List losses, - Optional writeResult - ) { - public ImportResult { - Objects.requireNonNull(message); - Objects.requireNonNull(kind); - Objects.requireNonNull(targetKey); - capabilities = Set.copyOf(capabilities); - losses = List.copyOf(losses); - Objects.requireNonNull(writeResult); - } - } - - public static final class StructureImportException extends Exception { - private final List losses; - - public StructureImportException(String message, Throwable cause, List losses) { - super(message, cause); - this.losses = List.copyOf(losses); - } - - public List losses() { - return losses; - } - } - - private static Path normalizedRoot(Path path) { - return Objects.requireNonNull(path).toAbsolutePath().normalize(); - } - - private static StructureSource.Kind inferredSourceKind(StructureKey sourceKey) { - return sourceKey.namespace().equals("minecraft") - ? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK; - } -} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCapture.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCapture.java deleted file mode 100644 index aa85e8a36..000000000 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCapture.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.modded.structure; - -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureKey; -import art.arcane.iris.core.structure.authoring.StructureLoss; -import art.arcane.iris.engine.object.IrisObject; -import art.arcane.iris.modded.ModdedBlockResolution; -import art.arcane.iris.modded.ModdedBlockState; -import art.arcane.iris.modded.ModdedTileData; -import net.minecraft.core.HolderGetter; -import net.minecraft.core.Vec3i; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.NbtUtils; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -final class ModdedStructureTemplateCapture { - private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - - private ModdedStructureTemplateCapture() { - } - - static Capture capture( - StructureKey sourceKey, - StructureTemplate template, - HolderGetter blockLookup, - boolean connectorsPreserved - ) { - return capture(sourceKey, template, blockLookup, connectorsPreserved, true); - } - - static Capture capture( - StructureKey sourceKey, - StructureTemplate template, - HolderGetter blockLookup, - boolean connectorsPreserved, - boolean includeAir - ) { - Objects.requireNonNull(template); - CompoundTag sourceTag = template.save(new CompoundTag()); - return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, includeAir); - } - - static Capture captureTag( - StructureKey sourceKey, - CompoundTag sourceTag, - HolderGetter blockLookup, - boolean connectorsPreserved - ) { - return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, true); - } - - static Capture captureTag( - StructureKey sourceKey, - CompoundTag sourceTag, - HolderGetter blockLookup, - boolean connectorsPreserved, - boolean includeAir - ) { - Objects.requireNonNull(sourceKey); - Objects.requireNonNull(sourceTag); - Objects.requireNonNull(blockLookup); - Vec3i size = readSize(sourceTag); - IrisObject object = new IrisObject(size.getX(), size.getY(), size.getZ()); - List losses = new ArrayList<>(); - ListTag palette = firstPalette(sourceTag, losses); - ListTag blocks = sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG); - CaptureCounts counts = captureBlocks(sourceKey, palette, blocks, blockLookup, object, losses, includeAir); - recordEntityLoss(sourceTag, losses); - recordMarkerLosses(counts, connectorsPreserved, losses); - - EnumSet capabilities = EnumSet.of(StructureCapability.BLOCKS); - if (counts.tiles() > 0) { - capabilities.add(StructureCapability.BLOCK_ENTITIES); - } - if (connectorsPreserved && counts.jigsaws() > 0) { - capabilities.add(StructureCapability.CONNECTORS); - } - return new Capture( - object, - counts.blocks(), - counts.tiles(), - counts.jigsaws(), - counts.dataMarkers(), - size.getX(), - size.getY(), - size.getZ(), - capabilities, - losses, - sourceTag.copy() - ); - } - - private static Vec3i readSize(CompoundTag sourceTag) { - ListTag size = sourceTag.getListOrEmpty(StructureTemplate.SIZE_TAG); - int width = size.getIntOr(0, 0); - int height = size.getIntOr(1, 0); - int depth = size.getIntOr(2, 0); - if (width < 1 || height < 1 || depth < 1) { - throw new IllegalArgumentException("Structure template has invalid dimensions " - + width + "x" + height + "x" + depth); - } - return new Vec3i(width, height, depth); - } - - private static ListTag firstPalette(CompoundTag sourceTag, List losses) { - ListTag palettes = sourceTag.getList(StructureTemplate.PALETTE_LIST_TAG).orElse(null); - if (palettes != null) { - if (palettes.isEmpty()) { - throw new IllegalArgumentException("Structure template has no palettes"); - } - if (palettes.size() > 1) { - losses.add(StructureLoss.warning( - StructureCapability.BLOCKS, - "palette_variants_not_imported", - "Only palette 0 was converted; " + (palettes.size() - 1) - + " additional palette(s) remain native-only." - )); - } - return palettes.getListOrEmpty(0); - } - ListTag palette = sourceTag.getListOrEmpty(StructureTemplate.PALETTE_TAG); - if (palette.isEmpty() && !sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG).isEmpty()) { - throw new IllegalArgumentException("Structure template has blocks but no palette"); - } - return palette; - } - - private static CaptureCounts captureBlocks( - StructureKey sourceKey, - ListTag palette, - ListTag blocks, - HolderGetter blockLookup, - IrisObject object, - List losses, - boolean includeAir - ) { - int blockCount = 0; - int tiles = 0; - int jigsaws = 0; - int dataMarkers = 0; - for (int index = 0; index < blocks.size(); index++) { - CompoundTag blockTag = blocks.getCompoundOrEmpty(index); - BlockPosition position = readPosition(blockTag); - if (!withinObject(position, object)) { - losses.add(StructureLoss.warning( - StructureCapability.BLOCKS, - "out_of_bounds_block_not_imported", - "Block " + index + " at " + position.x() + "," + position.y() + "," + position.z() - + " is outside the declared template bounds." - )); - continue; - } - int paletteIndex = blockTag.getIntOr(StructureTemplate.BLOCK_TAG_STATE, 0); - BlockState state = NbtUtils.readBlockState(blockLookup, palette.getCompoundOrEmpty(paletteIndex)); - CompoundTag blockEntityTag = blockTag.getCompound(StructureTemplate.BLOCK_TAG_NBT).orElse(null); - if (state.is(Blocks.STRUCTURE_VOID)) { - continue; - } - if (state.is(Blocks.STRUCTURE_BLOCK)) { - dataMarkers++; - continue; - } - if (state.is(Blocks.JIGSAW)) { - jigsaws++; - BlockState finalState = resolveJigsawFinalState(sourceKey, position, blockEntityTag, losses); - if (finalState == null || finalState.isAir()) { - continue; - } - state = finalState; - blockEntityTag = null; - } - if (!includeAir && state.isAir()) { - continue; - } - object.setUnsigned(position.x(), position.y(), position.z(), ModdedBlockState.of(state, null)); - blockCount++; - if (blockEntityTag != null && captureTile(sourceKey, position, state, blockEntityTag, object, losses)) { - tiles++; - } - } - return new CaptureCounts(blockCount, tiles, jigsaws, dataMarkers); - } - - private static BlockPosition readPosition(CompoundTag blockTag) { - ListTag position = blockTag.getListOrEmpty(StructureTemplate.BLOCK_TAG_POS); - return new BlockPosition( - position.getIntOr(0, Integer.MIN_VALUE), - position.getIntOr(1, Integer.MIN_VALUE), - position.getIntOr(2, Integer.MIN_VALUE) - ); - } - - private static boolean withinObject(BlockPosition position, IrisObject object) { - return position.x() >= 0 && position.x() < object.getW() - && position.y() >= 0 && position.y() < object.getH() - && position.z() >= 0 && position.z() < object.getD(); - } - - private static BlockState resolveJigsawFinalState( - StructureKey sourceKey, - BlockPosition position, - CompoundTag blockEntityTag, - List losses - ) { - String finalState = blockEntityTag == null - ? "minecraft:air" - : blockEntityTag.getStringOr("final_state", "minecraft:air"); - try { - return ModdedBlockResolution.strictParse(finalState).handle(); - } catch (IllegalArgumentException failure) { - LOGGER.error("Failed to parse jigsaw final state '{}' in {} at {},{},{}", - finalState, sourceKey, position.x(), position.y(), position.z(), failure); - losses.add(StructureLoss.warning( - StructureCapability.BLOCKS, - "jigsaw_final_state_not_imported", - "Jigsaw final state '" + finalState + "' at " + position.x() + "," + position.y() + "," - + position.z() + " could not be parsed." - )); - return null; - } - } - - private static boolean captureTile( - StructureKey sourceKey, - BlockPosition position, - BlockState state, - CompoundTag blockEntityTag, - IrisObject object, - List losses - ) { - try { - String blockKey = ModdedBlockState.serialize(state); - ModdedTileData tile = ModdedTileData.capture(blockKey, NbtUtils.structureToSnbt(blockEntityTag)); - object.setUnsignedTile(position.x(), position.y(), position.z(), tile); - return true; - } catch (IOException | RuntimeException failure) { - LOGGER.error("Failed to capture block entity in {} at {},{},{}", - sourceKey, position.x(), position.y(), position.z(), failure); - losses.add(StructureLoss.warning( - StructureCapability.BLOCK_ENTITIES, - "block_entity_not_imported", - "Block entity data at " + position.x() + "," + position.y() + "," + position.z() - + " could not be encoded." - )); - return false; - } - } - - private static void recordEntityLoss(CompoundTag sourceTag, List losses) { - int entityCount = sourceTag.getListOrEmpty(StructureTemplate.ENTITIES_TAG).size(); - if (entityCount > 0) { - losses.add(StructureLoss.warning( - StructureCapability.ENTITIES, - "entities_not_imported", - entityCount + " structure entit" + (entityCount == 1 ? "y was" : "ies were") - + " not converted into the Iris snapshot." - )); - } - } - - private static void recordMarkerLosses( - CaptureCounts counts, - boolean connectorsPreserved, - List losses - ) { - if (counts.dataMarkers() > 0) { - losses.add(StructureLoss.warning( - StructureCapability.PROCESSORS, - "data_markers_not_imported", - counts.dataMarkers() + " structure data marker(s) require native pool-element handlers and were omitted." - )); - } - if (!connectorsPreserved && counts.jigsaws() > 0) { - losses.add(StructureLoss.warning( - StructureCapability.CONNECTORS, - "connectors_not_imported", - counts.jigsaws() + " jigsaw connector(s) were resolved to final blocks without importing their pool graph." - )); - } - } - - record Capture( - IrisObject object, - int blocks, - int tiles, - int jigsaws, - int dataMarkers, - int width, - int height, - int depth, - Set capabilities, - List losses, - CompoundTag sourceTag - ) { - Capture { - Objects.requireNonNull(object); - capabilities = Set.copyOf(capabilities); - losses = List.copyOf(losses); - sourceTag = sourceTag.copy(); - } - } - - private record CaptureCounts(int blocks, int tiles, int jigsaws, int dataMarkers) { - } - - private record BlockPosition(int x, int y, int z) { - } -} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java index e2b5f5e46..9ed4ebd45 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedChunkGeneratorSpawnTest.java @@ -58,8 +58,8 @@ public class IrisModdedChunkGeneratorSpawnTest { int spawnEnd = source.indexOf("@Override", spawnStart + 1); String spawn = source.substring(spawnStart, spawnEnd); - assertTrue(spawn.contains("initializeVanillaSpawnBiomes(registry)")); - assertTrue(spawn.contains("vanillaSpawnBiomes.get(visibleBiome.value())")); + assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)")); + assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())")); assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration(")); assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())")); assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()")); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java index 010d5153e..4e6b92975 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java @@ -58,10 +58,10 @@ public class IrisModdedStructureParityTest { @Test public void spawnHeightMatchesPaperFixedSpawnClamp() { - assertEquals(96, IrisModdedChunkGenerator.clampSpawnHeight(-64, 384)); - assertEquals(96, IrisModdedChunkGenerator.clampSpawnHeight(0, 128)); - assertEquals(88, IrisModdedChunkGenerator.clampSpawnHeight(80, 10)); - assertEquals(101, IrisModdedChunkGenerator.clampSpawnHeight(100, 20)); + assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(-64, 384)); + assertEquals(96, ModdedDimensionMetadata.clampSpawnHeight(0, 128)); + assertEquals(88, ModdedDimensionMetadata.clampSpawnHeight(80, 10)); + assertEquals(101, ModdedDimensionMetadata.clampSpawnHeight(100, 20)); } @Test @@ -109,7 +109,7 @@ public class IrisModdedStructureParityTest { .setVanillaDerivative("minecraft:plains") .setInferredType(InferredType.SEA); - Set keys = IrisModdedChunkGenerator.collectConfiguredBiomeKeys( + Set keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys( List.of(ocean, custom, shore, unsafeSea), "OverWorld"); assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach", @@ -150,8 +150,8 @@ public class IrisModdedStructureParityTest { .setFluidHeight(50); dimension.setLoadKey("bootstrap_contract"); - IrisModdedChunkGenerator.DimensionMetadata metadata = - IrisModdedChunkGenerator.dimensionMetadata(dimension); + ModdedDimensionMetadata.DimensionMetadata metadata = + ModdedDimensionMetadata.dimensionMetadata(dimension); assertEquals(-256, metadata.minY()); assertEquals(512, metadata.maxY()); @@ -161,8 +161,8 @@ public class IrisModdedStructureParityTest { @Test public void structureRingWorkersWaitWithoutBlockingLifecycleBinding() throws Exception { - IrisModdedChunkGenerator.EngineBinding binding = - new IrisModdedChunkGenerator.EngineBinding<>(5L, TimeUnit.SECONDS); + ModdedEngineBinding binding = + new ModdedEngineBinding<>(5L, TimeUnit.SECONDS); String exactEngine = "exact-engine"; CountDownLatch workerStarted = new CountDownLatch(1); ExecutorService executor = Executors.newSingleThreadExecutor(); @@ -184,8 +184,8 @@ public class IrisModdedStructureParityTest { @Test public void structureRingBindingPropagatesBootstrapFailure() { - IrisModdedChunkGenerator.EngineBinding binding = - new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS); + ModdedEngineBinding binding = + new ModdedEngineBinding<>(1L, TimeUnit.SECONDS); IllegalArgumentException failure = new IllegalArgumentException("broken pack"); binding.fail(failure); @@ -200,16 +200,16 @@ public class IrisModdedStructureParityTest { @Test public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() { - IrisModdedChunkGenerator.EngineBinding binding = - new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS); + ModdedEngineBinding binding = + new ModdedEngineBinding<>(1L, TimeUnit.SECONDS); binding.throwIfFailed("overworld:overworld"); } @Test public void structureBiomeBootstrapPropagatesBindingFailure() { - IrisModdedChunkGenerator.EngineBinding binding = - new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS); + ModdedEngineBinding binding = + new ModdedEngineBinding<>(1L, TimeUnit.SECONDS); IllegalArgumentException failure = new IllegalArgumentException("broken pack"); binding.fail(failure); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java index 68662d2a9..9bb4f97a4 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedDimensionTypeParityTest.java @@ -152,13 +152,13 @@ public class ModdedDimensionTypeParityTest { .ceiling(TRUE); IrisDimension dimension = dimension("runtime_contract", IrisEnvironment.CUSTOM, -128, 384, 384, options); - ModdedWorldCheck.DimensionContract expected = ModdedWorldCheck.expectedDimensionContract(dimension); - ModdedWorldCheck.DimensionContract fallback = new ModdedWorldCheck.DimensionContract( + WorldCheckDimensionContract.DimensionContract expected = WorldCheckDimensionContract.expectedDimensionContract(dimension); + WorldCheckDimensionContract.DimensionContract fallback = new WorldCheckDimensionContract.DimensionContract( -256, 768, 512, 1D, 0F, true, false, false, 0); - assertTrue(ModdedWorldCheck.matchesDimensionContract(-128, 512, expected, expected)); - assertFalse(ModdedWorldCheck.matchesDimensionContract(-256, 768, expected, fallback)); - assertFalse(ModdedWorldCheck.matchesDimensionContract(-128, 512, expected, fallback)); + assertTrue(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, expected)); + assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-256, 768, expected, fallback)); + assertFalse(WorldCheckDimensionContract.matchesDimensionContract(-128, 512, expected, fallback)); } private static IrisDimension dimension(String key, IrisEnvironment environment, int minY, int maxY, diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java index 175da412b..35899b5a8 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedWorldCheckTest.java @@ -28,40 +28,43 @@ public class ModdedWorldCheckTest { @Test public void poiAuditRunsInASecondServerTaskAfterVillageGeneration() throws IOException { - Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), - "art/arcane/iris/modded/ModdedWorldCheck.java"); - String source = Files.readString(sourcePath); + Path sourceRoot = Path.of(System.getProperty("iris.moddedCommonSources")); + String source = Files.readString(sourceRoot.resolve("art/arcane/iris/modded/ModdedWorldCheck.java")); + String auditSource = Files.readString( + sourceRoot.resolve("art/arcane/iris/modded/WorldCheckStructureAudit.java")); int preparationSubmit = source.indexOf( "WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();"); int completionSubmit = source.indexOf( "exitCode = serverRef.submit(() -> runAndRequestStop(", preparationSubmit); int completionMethod = source.indexOf("private static boolean completeWorldCheck"); - int deferredAudit = source.indexOf("PoiAudit poi = auditStructurePois", completionMethod); - int structureMethod = source.indexOf("private static StructureCheckResult checkNativeStructure"); - int structureMethodEnd = source.indexOf("private static StructureStart resolveStructureStart", + int deferredAudit = source.indexOf( + "PoiAudit poi = WorldCheckStructureAudit.auditStructurePois", completionMethod); + int structureMethod = auditSource.indexOf("private static StructureCheckResult checkNativeStructure"); + int structureMethodEnd = auditSource.indexOf("private static StructureStart resolveStructureStart", structureMethod); - String structureSource = source.substring(structureMethod, structureMethodEnd); + String structureSource = auditSource.substring(structureMethod, structureMethodEnd); assertTrue(preparationSubmit >= 0); assertTrue(completionSubmit > preparationSubmit); assertTrue(deferredAudit > completionMethod); assertFalse(structureSource.contains("auditStructurePois")); assertFalse(source.contains("prepareDeferredAudits")); + assertFalse(auditSource.contains("prepareDeferredAudits")); } @Test public void validStructureStartIsGenerationEvidence() { - assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(true, 0)); + assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(true, 0)); } @Test public void structureReferenceIsGenerationEvidence() { - assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(false, 1)); + assertTrue(WorldCheckPredicates.hasNativeStructureEvidence(false, 1)); } @Test public void absentStartAndReferencesFailGenerationEvidence() { - assertFalse(ModdedWorldCheck.hasNativeStructureEvidence(false, 0)); + assertFalse(WorldCheckPredicates.hasNativeStructureEvidence(false, 0)); } @Test @@ -91,68 +94,68 @@ public class ModdedWorldCheckTest { @Test public void materialEvidenceMustExist() { - assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(0, 0, 1)); - assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 0, 1)); - assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 0)); - assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 2, 1)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(0, 0, 1)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 0, 1)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 0)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 1)); } @Test public void singleChunkStructureAcceptsMaterialInItsOnlyChunk() { - assertTrue(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 1)); + assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 1)); } @Test public void multiChunkStructureRejectsMaterialConfinedToOneChunk() { - assertFalse(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 1, 4)); - assertTrue(ModdedWorldCheck.hasCharacteristicMaterialEvidence(8, 2, 4)); + assertFalse(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 1, 4)); + assertTrue(WorldCheckPredicates.hasCharacteristicMaterialEvidence(8, 2, 4)); } @Test public void configuredVerticalShiftRequiresSafetyClampedGenerationEvidence() { - assertTrue(ModdedWorldCheck.verticalShiftMatches(0, null, -32, 20, -64, 320)); - assertFalse(ModdedWorldCheck.verticalShiftMatches(0, null, -112, -80, -64, 320)); - assertTrue(ModdedWorldCheck.verticalShiftMatches(0, 0, -32, 20, -64, 320)); - assertTrue(ModdedWorldCheck.verticalShiftMatches(0, 48, -64, -32, -64, 320)); - assertTrue(ModdedWorldCheck.verticalShiftMatches(-64, -64, -48, 4, -64, 320)); - assertTrue(ModdedWorldCheck.verticalShiftMatches(-64, -16, -64, -12, -64, 320)); - assertFalse(ModdedWorldCheck.verticalShiftMatches(-64, null, -48, 4, -64, 320)); - assertFalse(ModdedWorldCheck.verticalShiftMatches(-64, -15, -63, -11, -64, 320)); - assertFalse(ModdedWorldCheck.verticalShiftMatches(0, -1, -33, 19, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(0, null, -32, 20, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(0, null, -112, -80, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 0, -32, 20, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(0, 48, -64, -32, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -64, -48, 4, -64, 320)); + assertTrue(WorldCheckPredicates.verticalShiftMatches(-64, -16, -64, -12, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, null, -48, 4, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(-64, -15, -63, -11, -64, 320)); + assertFalse(WorldCheckPredicates.verticalShiftMatches(0, -1, -33, 19, -64, 320)); } @Test public void mansionVegetationGateRejectsRemainingLeaves() { - assertTrue(ModdedWorldCheck.mansionVegetationPass(0)); - assertFalse(ModdedWorldCheck.mansionVegetationPass(1)); + assertTrue(WorldCheckPredicates.mansionVegetationPass(0)); + assertFalse(WorldCheckPredicates.mansionVegetationPass(1)); } @Test public void mansionVegetationAuditIgnoresTemplateBlocksAndRejectsVegetationAbovePieces() { - assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(true, 80, 80)); - assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(true, 79, 80)); - assertTrue(ModdedWorldCheck.mansionVegetationAbovePiece(true, 81, 80)); - assertFalse(ModdedWorldCheck.mansionVegetationAbovePiece(false, 81, 80)); + assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 80, 80)); + assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(true, 79, 80)); + assertTrue(WorldCheckPredicates.mansionVegetationAbovePiece(true, 81, 80)); + assertFalse(WorldCheckPredicates.mansionVegetationAbovePiece(false, 81, 80)); } @Test public void villageFoundationGateRejectsUnsupportedColumns() { - assertTrue(ModdedWorldCheck.villageFoundationPass(0)); - assertFalse(ModdedWorldCheck.villageFoundationPass(1)); + assertTrue(WorldCheckPredicates.villageFoundationPass(0)); + assertFalse(WorldCheckPredicates.villageFoundationPass(1)); } @Test public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() { - assertTrue(ModdedWorldCheck.villagePoiPass(1, 0)); - assertFalse(ModdedWorldCheck.villagePoiPass(0, 0)); - assertFalse(ModdedWorldCheck.villagePoiPass(1, 1)); + assertTrue(WorldCheckPredicates.villagePoiPass(1, 0)); + assertFalse(WorldCheckPredicates.villagePoiPass(0, 0)); + assertFalse(WorldCheckPredicates.villagePoiPass(1, 1)); } @Test public void smallStructureFootprintIncludesEveryChunk() { BoundingBox bounds = new BoundingBox(-16, -20, -16, 31, 120, 31); - List chunks = ModdedWorldCheck.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96); + List chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 96); assertEquals(9, chunks.size()); assertTrue(chunks.contains(new ChunkPos(-1, -1))); @@ -163,7 +166,7 @@ public class ModdedWorldCheckTest { public void largeStructureFootprintIsBoundedAndSamplesEdges() { BoundingBox bounds = new BoundingBox(-512, -64, -512, 511, 300, 511); - List chunks = ModdedWorldCheck.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20); + List chunks = WorldCheckStructureAudit.boundedFootprintChunks(bounds, ChunkPos.ZERO, 20); assertTrue(chunks.size() <= 20); assertTrue(chunks.size() >= 16); @@ -174,7 +177,7 @@ public class ModdedWorldCheckTest { @Test public void qaEventsEscapeStructuredValues() { - String event = ModdedWorldCheck.qaEventJson("locate\"", "village\n", false, "x\\y\t"); + String event = WorldCheckPredicates.qaEventJson("locate\"", "village\n", false, "x\\y\t"); assertEquals("QA_EVT {\"event\":\"locate\\\"\",\"structure\":\"village\\n\"," + "\"pass\":false,\"detail\":\"x\\\\y\\t\"}", event); @@ -292,7 +295,7 @@ public class ModdedWorldCheckTest { } private static boolean characteristic(String structureLabel, String structureKey, String blockKey) { - return ModdedWorldCheck.isCharacteristicMaterial(structureLabel, + return WorldCheckMaterials.isCharacteristicMaterial(structureLabel, Identifier.parse(structureKey), Identifier.parse(blockKey)); } } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java index 9c9a8a34d..97ccec1aa 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/NativeStructureFailureContractTest.java @@ -24,15 +24,14 @@ public class NativeStructureFailureContractTest { @Test public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException { - Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), - "art/arcane/iris/modded/IrisModdedChunkGenerator.java"); - String source = Files.readString(sourcePath); + String source = moddedSource("IrisModdedChunkGenerator.java"); int locateStart = source.indexOf("public Pair> findNearestMapStructure"); int locateEnd = source.indexOf("public boolean isNativeStructureReachable", locateStart); String locate = source.substring(locateStart, locateEnd); - int filterStart = source.indexOf("private HolderSet filterReachableNativeStructures"); - int filterEnd = source.indexOf("private ServerLevel boundLevel", filterStart); - String filter = source.substring(filterStart, filterEnd); + String stage = moddedSource("ModdedNativeStructureStage.java"); + int filterStart = stage.indexOf("HolderSet filterReachableNativeStructures"); + int filterEnd = stage.indexOf("void adjustGeneratedStructures", filterStart); + String filter = stage.substring(filterStart, filterEnd); assertTrue(locate.contains("Engine current = engine();")); assertFalse(locate.contains("catch (Throwable")); @@ -58,10 +57,8 @@ public class NativeStructureFailureContractTest { @Test public void structureTerrainPreparationPrecedesVegetationAndPlacement() throws IOException { - Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), - "art/arcane/iris/modded/IrisModdedChunkGenerator.java"); - String source = Files.readString(sourcePath); - int placementStart = source.indexOf("private void placeVanillaStructures"); + String source = moddedSource("ModdedNativeStructureStage.java"); + int placementStart = source.indexOf("void placeVanillaStructures"); int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart); String placement = source.substring(placementStart, placementEnd); @@ -74,6 +71,23 @@ public class NativeStructureFailureContractTest { < placement.indexOf("for (NativePlacementGroup group")); } + @Test + public void structurePlacementPrimesNeighbourWorldgenHeightmapsBeforeTerrainPreparation() throws IOException { + String source = moddedSource("ModdedNativeStructureStage.java"); + int placementStart = source.indexOf("void placeVanillaStructures"); + int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart); + String placement = source.substring(placementStart, placementEnd); + + assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;")); + assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement(")); + assertTrue(placement.contains("\"heightmap priming\"")); + assertTrue(placement.contains("heightmapStarts.add(start);")); + assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(") + < placement.indexOf("prepareSurfaceStructures")); + assertTrue(source.contains("generationEngine.getHeight(x, z, false) + runtimeMinY + 1")); + assertTrue(source.contains("generationEngine.getHeight(x, z, true) + runtimeMinY + 1")); + } + @Test public void structureFailurePreservesPhaseIdentityChunkAndCause() { IllegalArgumentException cause = new IllegalArgumentException("broken placement"); @@ -86,4 +100,10 @@ public class NativeStructureFailureContractTest { assertTrue(error.getMessage().contains("12,-8")); assertTrue(error.getMessage().contains("aborted")); } + + private static String moddedSource(String fileName) throws IOException { + Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"), + "art", "arcane", "iris", "modded", fileName); + return Files.readString(sourcePath); + } } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java index 070db627b..78e93054d 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedStructureCommandTest.java @@ -12,7 +12,8 @@ import static org.junit.Assert.assertTrue; public class IrisModdedStructureCommandTest { @Test public void gotoStructureSupportsIrisAndNativeRegistryTargets() throws IOException { - String source = source("IrisModdedCommands.java"); + String source = source("ModdedLocateCommands.java"); + String suggestions = source("ModdedCommandSuggestions.java"); assertTrue(source.contains("IrisStructureLocator.isPlaced(engine, key)")); assertTrue(source.contains("registry.get(identifier)")); @@ -22,7 +23,7 @@ public class IrisModdedStructureCommandTest { assertTrue(source.contains("HolderSet.direct(target.holder())")); assertFalse(source.contains("NativeStructureLocateCapability")); assertTrue(source.contains("boolean teleported = player.teleportTo(")); - assertTrue(source.contains("combineStructureKeys(irisKeys, nativeKeys)")); + assertTrue(suggestions.contains("combineStructureKeys(irisKeys, nativeKeys)")); assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)")); assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED")); assertTrue(source.contains("IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS")); @@ -35,9 +36,9 @@ public class IrisModdedStructureCommandTest { @Test public void generatorLocateUsesEveryIrisPlacedNativeStructure() throws IOException { - String source = moddedSource("IrisModdedChunkGenerator.java"); - int methodStart = source.indexOf("private Pair> findNearestIrisStructure("); - int methodEnd = source.indexOf("private HolderSet filterReachableNativeStructures(", methodStart); + String source = moddedSource("ModdedNativeStructureStage.java"); + int methodStart = source.indexOf("Pair> findNearestIrisStructure("); + int methodEnd = source.indexOf("HolderSet filterReachableNativeStructures(", methodStart); String method = source.substring(methodStart, methodEnd); int unexploredGuard = method.indexOf("if (findUnexplored)"); int registryLookup = method.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)"); @@ -58,8 +59,8 @@ public class IrisModdedStructureCommandTest { @Test public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException { - String source = source("IrisModdedCommands.java"); - int methodStart = source.indexOf("private static int gotoStructure("); + String source = source("ModdedLocateCommands.java"); + int methodStart = source.indexOf("static int gotoStructure("); int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart); String method = source.substring(methodStart, methodEnd); int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)"); @@ -79,7 +80,7 @@ public class IrisModdedStructureCommandTest { @Test public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException { - String source = source("IrisModdedCommands.java"); + String source = source("ModdedLocateCommands.java"); int methodStart = source.indexOf("private static int verifyStructure("); int methodEnd = source.indexOf("private static Optional resolveNativeStructure(", methodStart); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCaptureTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCaptureTest.java deleted file mode 100644 index 305a52385..000000000 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedJigsawStructureCaptureTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.modded.structure; - -import net.minecraft.core.Direction; -import org.junit.Test; - -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - -public class ModdedJigsawStructureCaptureTest { - @Test - public void namespacedSourcePathsRemainDistinctAndPortable() { - String nested = ModdedJigsawStructureCapture.pieceName("village", "mod:a/b"); - String underscored = ModdedJigsawStructureCapture.pieceName("village", "mod_a:b"); - - assertEquals("village/piece/mod/a/b", nested); - assertEquals("village/piece/mod_a/b", underscored); - assertNotEquals(nested, underscored); - assertEquals("village/pool/mod/a/b", ModdedJigsawStructureCapture.poolName("village", "mod:a/b")); - assertEquals( - "village/piece/generated/legacy/mod/a/b", - ModdedJigsawStructureCapture.legacyPieceName("village", "mod:a/b") - ); - } - - @Test - public void rootJsonRetainsGraphLimitsAndSourceIdentity() { - Map root = ModdedJigsawStructureCapture.structureJson( - "minecraft:village_plains", - "village/pool/minecraft/village/plains/town_centers", - 6, - 81 - ); - - assertEquals("minecraft:village_plains", root.get("vanillaSource")); - assertEquals(6, root.get("maxDepth")); - assertEquals(6, root.get("maxSizeChunks")); - assertEquals("STRUCTURE_PIECE", root.get("placeMode")); - } - - @Test - public void connectorDirectionsUseIrisAxisNames() { - assertEquals("UP_POSITIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.UP)); - assertEquals("DOWN_NEGATIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.DOWN)); - assertEquals("NORTH_NEGATIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.NORTH)); - assertEquals("SOUTH_POSITIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.SOUTH)); - assertEquals("EAST_POSITIVE_X", ModdedJigsawStructureCapture.directionName(Direction.EAST)); - assertEquals("WEST_NEGATIVE_X", ModdedJigsawStructureCapture.directionName(Direction.WEST)); - } -} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureImportServiceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureImportServiceTest.java deleted file mode 100644 index 5b50d7a89..000000000 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureImportServiceTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.modded.structure; - -import art.arcane.iris.core.structure.authoring.StructureBackend; -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureKey; -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; -import art.arcane.iris.core.structure.authoring.StructureSource; -import art.arcane.iris.core.structure.authoring.StructureWriteMode; -import art.arcane.iris.core.structure.authoring.StructureWriteOptions; -import art.arcane.iris.core.structure.authoring.StructureWriteResult; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.nio.file.Path; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ModdedStructureImportServiceTest { - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void writesThroughOwnedAddOnlyAndOverwriteTransactions() throws Exception { - Path root = temporaryFolder.newFolder("modded-import").toPath(); - ModdedStructureImportService service = new ModdedStructureImportService(() -> null); - ModdedStructureImportService.PreparedImport first = prepared(root, "one", StructureWriteMode.ADD_ONLY); - - ModdedStructureImportService.ImportResult added = service.write(first); - ModdedStructureImportService.ImportResult conflict = service.write(first); - ModdedStructureImportService.ImportResult overwritten = service.write( - prepared(root, "two", StructureWriteMode.OVERWRITE) - ); - - assertTrue(added.success()); - assertEquals(StructureWriteResult.Status.ADDED, added.writeResult().orElseThrow().status()); - assertFalse(conflict.success()); - assertEquals(StructureWriteResult.Status.ADD_ONLY_CONFLICT, conflict.writeResult().orElseThrow().status()); - assertTrue(overwritten.success()); - assertEquals(StructureWriteResult.Status.OVERWRITTEN, overwritten.writeResult().orElseThrow().status()); - assertTrue(overwritten.capabilities().contains(StructureCapability.BLOCKS)); - } - - private static ModdedStructureImportService.PreparedImport prepared( - Path root, - String content, - StructureWriteMode mode - ) { - StructureKey key = StructureKey.parse("iris:test_structure"); - StructureResourceBundle bundle = StructureResourceBundle.builder(key) - .source(StructureSource.of(StructureSource.Kind.DATAPACK, StructureKey.parse("test:source"))) - .backend(StructureBackend.SNAPSHOT) - .capability(StructureCapability.BLOCKS) - .textResource("structures/test_structure.json", content) - .build(); - return new ModdedStructureImportService.PreparedImport( - root, - new StructureWriteOptions(mode, false), - ModdedStructureImportService.ImportKind.TEMPLATE, - bundle, - 1, - 1, - 1 - ); - } -} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCaptureTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCaptureTest.java deleted file mode 100644 index 27be9eaa8..000000000 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/structure/ModdedStructureTemplateCaptureTest.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.modded.structure; - -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureKey; -import net.minecraft.SharedConstants; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.IntTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.NbtUtils; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; -import net.minecraft.server.Bootstrap; -import org.junit.BeforeClass; -import org.junit.Test; - -import java.io.ByteArrayOutputStream; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ModdedStructureTemplateCaptureTest { - @BeforeClass - public static void bootstrap() { - SharedConstants.tryDetectVersion(); - Bootstrap.bootStrap(); - } - - @Test - public void capturesBlocksTilesAndExplicitStandaloneLosses() throws Exception { - CompoundTag template = templateTag(); - - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag( - StructureKey.parse("minecraft:test/template"), - template, - BuiltInRegistries.BLOCK, - false - ); - - assertEquals(4, capture.width()); - assertEquals(1, capture.height()); - assertEquals(1, capture.depth()); - assertEquals(3, capture.blocks()); - assertEquals(1, capture.tiles()); - assertEquals(1, capture.jigsaws()); - assertEquals(1, capture.dataMarkers()); - assertEquals(3, capture.object().getBlocks().size()); - assertEquals(1, capture.object().getStates().size()); - assertTrue(capture.capabilities().contains(StructureCapability.BLOCKS)); - assertTrue(capture.capabilities().contains(StructureCapability.BLOCK_ENTITIES)); - assertFalse(capture.capabilities().contains(StructureCapability.CONNECTORS)); - assertTrue(hasLoss(capture, "connectors_not_imported")); - assertTrue(hasLoss(capture, "data_markers_not_imported")); - assertTrue(hasLoss(capture, "entities_not_imported")); - ByteArrayOutputStream serialized = new ByteArrayOutputStream(); - capture.object().write(serialized); - assertTrue(serialized.size() > 0); - } - - @Test - public void graphCaptureReportsConnectorCapabilityWithoutStandaloneConnectorLoss() { - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag( - StructureKey.parse("minecraft:test/template"), - templateTag(), - BuiltInRegistries.BLOCK, - true - ); - - assertTrue(capture.capabilities().contains(StructureCapability.CONNECTORS)); - assertFalse(hasLoss(capture, "connectors_not_imported")); - } - - @Test - public void reportsAdditionalNativePalettes() { - CompoundTag template = templateTag(); - ListTag palettes = new ListTag(); - ListTag first = template.getListOrEmpty(StructureTemplate.PALETTE_TAG); - palettes.add(first.copy()); - palettes.add(first.copy()); - template.remove(StructureTemplate.PALETTE_TAG); - template.put(StructureTemplate.PALETTE_LIST_TAG, palettes); - - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag( - StructureKey.parse("minecraft:test/template"), - template, - BuiltInRegistries.BLOCK, - true - ); - - assertTrue(hasLoss(capture, "palette_variants_not_imported")); - } - - @Test - public void legacyCaptureOmitsAirBlocks() { - CompoundTag template = new CompoundTag(); - template.put(StructureTemplate.SIZE_TAG, intList(1, 1, 1)); - ListTag palette = new ListTag(); - palette.add(NbtUtils.writeBlockState(Blocks.AIR.defaultBlockState())); - template.put(StructureTemplate.PALETTE_TAG, palette); - ListTag blocks = new ListTag(); - blocks.add(block(0, 0, 0, 0, null)); - template.put(StructureTemplate.BLOCKS_TAG, blocks); - - ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag( - StructureKey.parse("minecraft:test/legacy"), - template, - BuiltInRegistries.BLOCK, - true, - false - ); - - assertEquals(0, capture.blocks()); - assertTrue(capture.object().getBlocks().isEmpty()); - } - - private static CompoundTag templateTag() { - CompoundTag template = new CompoundTag(); - template.put(StructureTemplate.SIZE_TAG, intList(4, 1, 1)); - ListTag palette = new ListTag(); - palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState())); - palette.add(NbtUtils.writeBlockState(Blocks.CHEST.defaultBlockState())); - palette.add(NbtUtils.writeBlockState(Blocks.JIGSAW.defaultBlockState())); - palette.add(NbtUtils.writeBlockState(Blocks.STRUCTURE_BLOCK.defaultBlockState())); - template.put(StructureTemplate.PALETTE_TAG, palette); - - ListTag blocks = new ListTag(); - blocks.add(block(0, 0, 0, 0, null)); - CompoundTag chest = new CompoundTag(); - chest.putString("id", "minecraft:chest"); - chest.putString("CustomName", "test"); - blocks.add(block(1, 0, 0, 1, chest)); - CompoundTag jigsaw = new CompoundTag(); - jigsaw.putString("final_state", "minecraft:oak_planks"); - blocks.add(block(2, 0, 0, 2, jigsaw)); - blocks.add(block(3, 0, 0, 3, new CompoundTag())); - template.put(StructureTemplate.BLOCKS_TAG, blocks); - - ListTag entities = new ListTag(); - entities.add(new CompoundTag()); - template.put(StructureTemplate.ENTITIES_TAG, entities); - return template; - } - - private static CompoundTag block(int x, int y, int z, int state, CompoundTag nbt) { - CompoundTag block = new CompoundTag(); - block.put(StructureTemplate.BLOCK_TAG_POS, intList(x, y, z)); - block.putInt(StructureTemplate.BLOCK_TAG_STATE, state); - if (nbt != null) { - block.put(StructureTemplate.BLOCK_TAG_NBT, nbt); - } - return block; - } - - private static ListTag intList(int... values) { - ListTag list = new ListTag(); - for (int value : values) { - list.add(IntTag.valueOf(value)); - } - return list; - } - - private static boolean hasLoss(ModdedStructureTemplateCapture.Capture capture, String code) { - return capture.losses().stream().anyMatch(loss -> loss.code().equals(code)); - } -} diff --git a/adapters/neoforge/gradle.properties b/adapters/neoforge/gradle.properties new file mode 100644 index 000000000..af258fbfc --- /dev/null +++ b/adapters/neoforge/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.caching=true +org.gradle.configuration-cache=false diff --git a/adapters/neoforge/settings.gradle b/adapters/neoforge/settings.gradle index df0960044..e8ebcffd5 100644 --- a/adapters/neoforge/settings.gradle +++ b/adapters/neoforge/settings.gradle @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import java.io.File - pluginManagement { repositories { maven { @@ -43,49 +41,8 @@ dependencyResolutionManagement { } } -boolean hasVolmLibSettings(File directory) { - new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists() -} - -File resolveLocalVolmLibDirectory() { - String configuredPath = providers.gradleProperty('localVolmLibDirectory') - .orElse(providers.environmentVariable('VOLMLIB_DIR')) - .orNull - if (configuredPath != null && !configuredPath.isBlank()) { - File configuredDirectory = file(configuredPath) - if (hasVolmLibSettings(configuredDirectory)) { - return configuredDirectory - } - } - - File currentDirectory = settingsDir - while (currentDirectory != null) { - File candidate = new File(currentDirectory, 'VolmLib') - if (hasVolmLibSettings(candidate)) { - return candidate - } - - currentDirectory = currentDirectory.parentFile - } - - null -} - -boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib') - .orElse('true') - .map { String value -> value.equalsIgnoreCase('true') } - .get() -File localVolmLibDirectory = resolveLocalVolmLibDirectory() - -if (useLocalVolmLib && localVolmLibDirectory != null) { - includeBuild(localVolmLibDirectory) { - dependencySubstitution { - substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared')) - } - } -} +// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle. +apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile includeBuild('../..') { dependencySubstitution { diff --git a/build.gradle b/build.gradle index eef8754a0..d232377dc 100644 --- a/build.gradle +++ b/build.gradle @@ -73,8 +73,11 @@ String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${load String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") apply plugin: ApiGenerator +// Where `buildAll` drops the per-platform jars for a local test server. Defaults to a repo-local +// directory so the task works on a fresh clone; point it at a real server tree with +// `-Plocation=/path/to/consumers`. String consumerLocation = providers.gradleProperty('location') - .getOrElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers') + .getOrElse(layout.buildDirectory.dir('consumers').get().asFile.absolutePath) String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins" String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods" String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods" diff --git a/core/purity-allowlist.txt b/core/purity-allowlist.txt index 7ddbbc60e..68465ca64 100644 --- a/core/purity-allowlist.txt +++ b/core/purity-allowlist.txt @@ -40,6 +40,7 @@ art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java art/arcane/iris/core/pregenerator/methods/HybridPregenMethod.java art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java art/arcane/iris/core/project/IrisProject.java +art/arcane/iris/core/project/StudioOpenProgressReporter.java art/arcane/iris/core/runtime/BukkitPublicRuntimeControlBackend.java art/arcane/iris/core/runtime/ChunkClearer.java art/arcane/iris/core/runtime/ChunkJobReporter.java @@ -72,6 +73,11 @@ art/arcane/iris/core/tools/IrisToolbelt.java art/arcane/iris/core/tools/IrisWorldCreator.java art/arcane/iris/engine/IrisEngineEffects.java art/arcane/iris/engine/IrisWorldManager.java +art/arcane/iris/engine/MarkerSpawnScanner.java +art/arcane/iris/engine/WorldBlockDropRouter.java +art/arcane/iris/engine/WorldChunkMaintenance.java +art/arcane/iris/engine/WorldEntitySpawner.java +art/arcane/iris/engine/WorldTeleportWarmup.java art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java art/arcane/iris/engine/data/chunk/TerrainChunk.java art/arcane/iris/engine/decorator/DecoratorCore.java @@ -85,6 +91,7 @@ art/arcane/iris/engine/object/IPostBlockAccess.java art/arcane/iris/engine/object/IrisAttributeModifier.java art/arcane/iris/engine/object/IrisBiome.java art/arcane/iris/engine/object/IrisBiomeCustomParticle.java +art/arcane/iris/engine/object/IrisBiomeDerivatives.java art/arcane/iris/engine/object/IrisBiomeCustomSpawn.java art/arcane/iris/engine/object/IrisBlockDrops.java art/arcane/iris/engine/object/IrisCommandRegistry.java @@ -153,19 +160,12 @@ art/arcane/iris/util/common/misc/Bindings.java art/arcane/iris/util/common/nbt/mca/Chunk.java art/arcane/iris/util/common/nbt/mca/NBTWorld.java art/arcane/iris/util/common/plugin/Chunks.java -art/arcane/iris/util/common/plugin/CommandDummy.java -art/arcane/iris/util/common/plugin/IController.java art/arcane/iris/util/common/plugin/IrisService.java -art/arcane/iris/util/common/plugin/MortarCommand.java -art/arcane/iris/util/common/plugin/MortarPermission.java -art/arcane/iris/util/common/plugin/RouterCommand.java -art/arcane/iris/util/common/plugin/VirtualCommand.java art/arcane/iris/util/common/plugin/VolmitPlugin.java art/arcane/iris/util/common/plugin/VolmitSender.java art/arcane/iris/util/common/plugin/chunk/ChunkTickets.java art/arcane/iris/util/common/plugin/chunk/TicketHolder.java art/arcane/iris/util/common/reflect/KeyedType.java -art/arcane/iris/util/common/reflect/OldEnum.java art/arcane/iris/util/common/scheduling/J.java art/arcane/iris/util/project/hunk/Hunk.java art/arcane/iris/util/project/hunk/view/ChunkBiomeHunkView.java diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 338db3584..706d4fd00 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -31,10 +31,11 @@ import lombok.Data; import java.io.File; import java.io.IOException; -@SuppressWarnings("SynchronizeOnNonFinalField") @Data public class IrisSettings { - public static IrisSettings settings; + private static final Object SETTINGS_LOCK = new Object(); + private static final IrisSettings BOOTSTRAP_DEFAULTS = new IrisSettings(); + public static volatile IrisSettings settings; private IrisSettingsGeneral general = new IrisSettingsGeneral(); private IrisSettingsWorld world = new IrisSettingsWorld(); private IrisSettingsGUI gui = new IrisSettingsGUI(); @@ -55,53 +56,82 @@ public class IrisSettings { } public static IrisSettings get() { - if (settings != null) { - return settings; + IrisSettings current = settings; + + if (current != null) { + return current; } - settings = new IrisSettings(); + if (Thread.holdsLock(SETTINGS_LOCK)) { + // read() logs and does IO, and the logging path calls back into get(). + // Serve defaults instead of recursing into another disk read. + return BOOTSTRAP_DEFAULTS; + } + synchronized (SETTINGS_LOCK) { + current = settings; + + if (current != null) { + return current; + } + + current = read(); + settings = current; + return current; + } + } + + private static IrisSettings read() { + IrisSettings loaded = new IrisSettings(); File s = IrisPlatforms.get().dataFile("settings.json"); if (!s.exists()) { try { - IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4)); + IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4)); } catch (JSONException | IOException e) { e.printStackTrace(); IrisLogging.reportError(e); } - } else { - try { - String ss = IO.readAll(s); - settings = new Gson().fromJson(ss, IrisSettings.class); - migrateLegacyKeys(ss); - try { - IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4)); - } catch (IOException e) { - e.printStackTrace(); - } - } catch (Throwable ee) { - // IrisLogging.reportError(ee); causes a self-reference & stackoverflow - IrisLogging.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage()); - } + + return loaded; } - return settings; + try { + String ss = IO.readAll(s); + IrisSettings parsed = new Gson().fromJson(ss, IrisSettings.class); + + if (parsed != null) { + loaded = parsed; + } + + migrateLegacyKeys(loaded, ss); + + try { + IO.writeAll(s, new JSONObject(new Gson().toJson(loaded)).toString(4)); + } catch (IOException e) { + e.printStackTrace(); + } + } catch (Throwable ee) { + // IrisLogging.reportError(ee); causes a self-reference & stackoverflow + IrisLogging.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage()); + } + + return loaded; } - private static void migrateLegacyKeys(String rawJson) { + private static void migrateLegacyKeys(IrisSettings target, String rawJson) { JSONObject root = new JSONObject(rawJson); JSONObject worldObject = root.optJSONObject("world"); if (worldObject == null || !worldObject.has("anbientEntitySpawningSystem")) { return; } - settings.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", settings.getWorld().isAmbientEntitySpawningSystem())); + target.getWorld().setAmbientEntitySpawningSystem(worldObject.optBoolean("anbientEntitySpawningSystem", target.getWorld().isAmbientEntitySpawningSystem())); IrisLogging.info("Migrated legacy settings key world.anbientEntitySpawningSystem -> world.ambientEntitySpawningSystem"); } public static void invalidate() { - synchronized (settings) { + synchronized (SETTINGS_LOCK) { settings = null; } } @@ -110,7 +140,7 @@ public class IrisSettings { File s = IrisPlatforms.get().dataFile("settings.json"); try { - IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4)); + IO.writeAll(s, new JSONObject(new Gson().toJson(this)).toString(4)); } catch (JSONException | IOException e) { e.printStackTrace(); IrisLogging.reportError(e); @@ -158,7 +188,6 @@ public class IrisSettings { private static final int MIN_RESIDENT_TECTONIC_PLATES = 16; private static final double MANTLE_HEAP_FRACTION = 0.6D; private static final int REFERENCE_PLATE_MEGABYTES = 48; - public boolean useTicketQueue = true; public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; public int chunkLoadTimeoutSeconds = 15; @@ -276,7 +305,6 @@ public class IrisSettings { @Data public static class IrisSettingsGenerator { public String defaultWorldType = "overworld"; - public int maxBiomeChildDepth = 4; public boolean preventLeafDecay = true; } @@ -292,7 +320,6 @@ public class IrisSettings { @Data public static class IrisSettingsStudio { - public boolean studio = true; public boolean openVSCode = true; public boolean disableTimeAndWeather = true; public boolean entitySpawning = true; diff --git a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java index 7dbc52b48..32ffc4917 100644 --- a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java +++ b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java @@ -26,6 +26,7 @@ import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.datapack.ModrinthResolver.ResolvedDatapack; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; import art.arcane.iris.core.structure.BulkStructureImporter; import art.arcane.iris.core.structure.StructureImporter; import art.arcane.iris.engine.object.IrisDimension; @@ -45,11 +46,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -100,7 +104,7 @@ public final class DatapackIngestService { return; } try { - new IrisProject(data.getDataFolder()).updateWorkspace(); + new IrisCodeWorkspace(new IrisProject(data.getDataFolder())).updateWorkspace(); } catch (Throwable e) { IrisLogging.reportError(e); } @@ -319,14 +323,41 @@ public final class DatapackIngestService { if (!force && installed && stripStateMatches) { continue; } - if (!worldFolder.exists()) { - worldFolder.mkdirs(); + if (!worldFolder.isDirectory() && !worldFolder.mkdirs() && !worldFolder.isDirectory()) { + throw new IOException("Couldn't create datapacks folder " + worldFolder.getPath()); } - IO.delete(target); - IO.copyDirectory(stagedDir.toPath(), target.toPath()); - if (stripOverrides) { - stripVanillaStructureOverrides(target); - writeMarker(marker); + // Stage outside the datapacks folder so a crash mid-copy can't leave a half-written pack for Minecraft to load. + File pendingRoot = worldFolder.getParentFile() == null + ? new File(worldFolder, ".iris-datapack-install") + : new File(worldFolder.getParentFile(), ".iris-datapack-install"); + File pending = new File(pendingRoot, id); + IO.delete(pending); + try { + IO.copyDirectory(stagedDir.toPath(), pending.toPath()); + if (stripOverrides) { + stripVanillaStructureOverrides(pending); + writeMarker(new File(pending, OVERRIDES_STRIPPED_MARKER)); + } + if (!new File(pending, "pack.mcmeta").isFile()) { + throw new IOException("Staged datapack " + id + " is missing pack.mcmeta"); + } + IO.delete(target); + try { + move(pending.toPath(), target.toPath()); + } catch (IOException swapFailure) { + IrisLogging.warn("Couldn't swap staged datapack " + id + " into " + target.getPath() + " (" + swapFailure.getMessage() + "); copying instead"); + try { + IO.copyDirectory(pending.toPath(), target.toPath()); + } catch (UncheckedIOException copyFailure) { + IO.delete(target); + throw copyFailure.getCause(); + } + } + } catch (UncheckedIOException e) { + throw e.getCause(); + } finally { + IO.delete(pending); + pendingRoot.delete(); } } } @@ -372,12 +403,8 @@ public final class DatapackIngestService { } } - private static void writeMarker(File marker) { - try { - Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8); - } catch (IOException e) { - IrisLogging.reportError(e); - } + private static void writeMarker(File marker) throws IOException { + Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8); } private static void autoImportDatapackStructures() { @@ -622,21 +649,43 @@ public final class DatapackIngestService { } return manifest; } catch (Exception e) { - IrisLogging.reportError(e); + IrisLogging.reportError("Unreadable datapack manifest " + file.getPath() + + "; moving it to manifest.json.corrupt instead of overwriting it", e); + quarantine(file.toPath()); return new Manifest(); } } - private static void writeManifest(File root, Manifest manifest) { - File file = new File(root, "manifest.json"); + private static void quarantine(Path file) { try { - File parent = file.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - Files.writeString(file.toPath(), GSON.toJson(manifest), StandardCharsets.UTF_8); + move(file, file.resolveSibling(file.getFileName().toString() + ".corrupt")); } catch (IOException e) { - IrisLogging.reportError(e); + IrisLogging.reportError("Failed to move aside corrupt datapack manifest " + file, e); + } + } + + private static void writeManifest(File root, Manifest manifest) { + Path file = new File(root, "manifest.json").toPath(); + try { + Path parent = file.getParent(); + Files.createDirectories(parent); + Path temp = Files.createTempFile(parent, "manifest", ".json.tmp"); + try { + Files.writeString(temp, GSON.toJson(manifest), StandardCharsets.UTF_8); + move(temp, file); + } finally { + Files.deleteIfExists(temp); + } + } catch (IOException e) { + IrisLogging.reportError("Failed to write datapack manifest " + file, e); + } + } + + private static void move(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); } } diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java index 6b830a0b6..080941760 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -33,7 +33,6 @@ import art.arcane.iris.core.pregenerator.PregenPhaseTracker; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.engine.framework.Engine; -import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.MemoryMonitor; import art.arcane.volmlib.util.function.Consumer2; @@ -45,6 +44,7 @@ import art.arcane.iris.util.common.scheduling.J; import java.awt.Color; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -65,8 +65,8 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource { private final MemoryMonitor monitor; private final PregenTask task; private final boolean saving; - private final KList> onProgress = new KList<>(); - private final KList whenDone = new KList<>(); + private final List> onProgress = new CopyOnWriteArrayList<>(); + private final List whenDone = new CopyOnWriteArrayList<>(); private final IrisPregenerator pregenerator; private final Position2 min; private final Position2 max; @@ -91,6 +91,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource { instance.updateAndGet(old -> { if (old != null) { old.pregenerator.close(); + old.worker.interrupt(); old.close(); } return this; @@ -135,7 +136,10 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource { return false; } - J.a(inst.pregenerator::close); + J.a(() -> { + inst.pregenerator.close(); + inst.worker.interrupt(); + }); return true; } @@ -305,6 +309,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource { public void stop() { J.a(() -> { pregenerator.close(); + worker.interrupt(); close(); instance.compareAndSet(this, null); }); diff --git a/core/src/main/java/art/arcane/iris/core/gui/components/TileRender.java b/core/src/main/java/art/arcane/iris/core/gui/components/TileRender.java deleted file mode 100644 index e68a8213e..000000000 --- a/core/src/main/java/art/arcane/iris/core/gui/components/TileRender.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.gui.components; - -import lombok.Builder; -import lombok.Data; - -import java.awt.image.BufferedImage; - -@Builder -@Data -public class TileRender { - private BufferedImage image; - private int quality; -} diff --git a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java index a148d96d7..b6c9d630e 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java @@ -55,6 +55,12 @@ public class ImageResourceLoader extends ResourceLoader { try { PrecisionStopwatch p = PrecisionStopwatch.start(); BufferedImage bu = ImageIO.read(j); + + if (bu == null) { + IrisLogging.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + " (unsupported or corrupt image)"); + return null; + } + IrisImage img = new IrisImage(bu); img.setLoadFile(j); img.setLoader(manager); @@ -69,7 +75,7 @@ public class ImageResourceLoader extends ResourceLoader { } } - void getPNGFiles(File directory, Set m, HashSet visitedDirectories) { + void getPNGFiles(File directory, String prefix, Set m, HashSet visitedDirectories) { if (directory == null || !directory.exists()) { return; } @@ -88,9 +94,9 @@ public class ImageResourceLoader extends ResourceLoader { for (File file : listedFiles) { if (file.isFile() && file.getName().endsWith(".png")) { - m.add(file.getName().replaceAll("\\Q.png\\E", "")); + m.add(prefix + file.getName().replaceAll("\\Q.png\\E", "")); } else if (file.isDirectory()) { - getPNGFiles(file, m, visitedDirectories); + getPNGFiles(file, prefix + file.getName() + "/", m, visitedDirectories); } } } @@ -105,31 +111,10 @@ public class ImageResourceLoader extends ResourceLoader { KSet m = new KSet<>(); HashSet visitedDirectories = new HashSet<>(); - for (File i : getFolders()) { - getPNGFiles(i, m, visitedDirectories); + getPNGFiles(i, "", m, visitedDirectories); } -// for (File i : getFolders()) { -// for (File j : i.listFiles()) { -// if (j.isFile() && j.getName().endsWith(".png")) { -// m.add(j.getName().replaceAll("\\Q.png\\E", "")); -// } else if (j.isDirectory()) { -// for (File k : j.listFiles()) { -// if (k.isFile() && k.getName().endsWith(".png")) { -// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.png\\E", "")); -// } else if (k.isDirectory()) { -// for (File l : k.listFiles()) { -// if (l.isFile() && l.getName().endsWith(".png")) { -// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.png\\E", "")); -// } -// } -// } -// } -// } -// } -// } - KList v = new KList<>(m); possibleKeys = v.toArray(new String[0]); return possibleKeys; @@ -152,18 +137,10 @@ public class ImageResourceLoader extends ResourceLoader { return null; } - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } + File file = resolveFile(name, ".png"); - File file = new File(i, name + ".png"); - - if (file.exists()) { - return file; - } + if (file != null) { + return file; } IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); @@ -176,18 +153,10 @@ public class ImageResourceLoader extends ResourceLoader { } private IrisImage loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } + File file = resolveFile(name, ".png"); - File file = new File(i, name + ".png"); - - if (file.exists()) { - return loadFile(file, name); - } + if (file != null) { + return loadFile(file, name); } IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); 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 b9d1aff1b..6cc1703d7 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 @@ -94,7 +94,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { private final File dataFolder; private final int id; private final boolean datapackCompiler; - private boolean closed = false; + private volatile boolean closed = false; private ResourceLoader biomeLoader; private ResourceLoader lootLoader; private ResourceLoader regionLoader; @@ -116,7 +116,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { private Gson gson; private Gson snippetLoader; private GsonBuilder builder; - private KMap, ResourceLoader> loaders = new KMap<>(); + private volatile KMap, ResourceLoader> loaders = new KMap<>(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private final transient List engines = new ArrayList<>(); @@ -367,16 +367,14 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { synchronized (engines) { engines.clear(); } - if (dataLoaders.get(dataFolder) == this) { - dataLoaders.remove(dataFolder); - } + dataLoaders.remove(dataFolder, this); } public IrisData copy() { return IrisData.get(dataFolder); } - private ResourceLoader registerLoader(Class registrant) { + private ResourceLoader registerLoader(Class registrant, KMap, ResourceLoader> target) { try { IrisRegistrant rr = registrant.getConstructor().newInstance(); ResourceLoader r = null; @@ -397,7 +395,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { r = new ResourceLoader<>(dataFolder, this, rr.getFolderName(), rr.getTypeName(), registrant, options); } - loaders.put(registrant, r); + target.put(registrant, r); return r; } catch (Throwable e) { @@ -420,30 +418,31 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { .registerTypeAdapterFactory(this) .registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter()) .setPrettyPrinting(); - loaders.clear(); + KMap, ResourceLoader> replacement = new KMap<>(); File packs = dataFolder; packs.mkdirs(); recoverStructureTransactions(); - this.lootLoader = registerLoader(IrisLootTable.class); - this.spawnerLoader = registerLoader(IrisSpawner.class); - this.entityLoader = registerLoader(IrisEntity.class); - this.regionLoader = registerLoader(IrisRegion.class); - this.biomeLoader = registerLoader(IrisBiome.class); - this.modLoader = registerLoader(IrisMod.class); - this.dimensionLoader = registerLoader(IrisDimension.class); - this.generatorLoader = registerLoader(IrisGenerator.class); - this.markerLoader = registerLoader(IrisMarker.class); - this.blockLoader = registerLoader(IrisBlockData.class); - this.expressionLoader = registerLoader(IrisExpression.class); - this.objectLoader = registerLoader(IrisObject.class); - this.imageLoader = registerLoader(IrisImage.class); - this.matterLoader = registerLoader(IrisMatterObject.class); - this.structureLoader = registerLoader(IrisStructure.class); - this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class); - this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class); + this.lootLoader = registerLoader(IrisLootTable.class, replacement); + this.spawnerLoader = registerLoader(IrisSpawner.class, replacement); + this.entityLoader = registerLoader(IrisEntity.class, replacement); + this.regionLoader = registerLoader(IrisRegion.class, replacement); + this.biomeLoader = registerLoader(IrisBiome.class, replacement); + this.modLoader = registerLoader(IrisMod.class, replacement); + this.dimensionLoader = registerLoader(IrisDimension.class, replacement); + this.generatorLoader = registerLoader(IrisGenerator.class, replacement); + this.markerLoader = registerLoader(IrisMarker.class, replacement); + this.blockLoader = registerLoader(IrisBlockData.class, replacement); + this.expressionLoader = registerLoader(IrisExpression.class, replacement); + this.objectLoader = registerLoader(IrisObject.class, replacement); + this.imageLoader = registerLoader(IrisImage.class, replacement); + this.matterLoader = registerLoader(IrisMatterObject.class, replacement); + this.structureLoader = registerLoader(IrisStructure.class, replacement); + this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class, replacement); + this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class, replacement); builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter); gson = builder.create(); + loaders = replacement; for (Engine engine : getEngines()) { engine.hotload(); @@ -460,13 +459,14 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { .registerTypeAdapterFactory(this) .registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter()) .setPrettyPrinting(); - loaders.clear(); + KMap, ResourceLoader> replacement = new KMap<>(); dataFolder.mkdirs(); recoverStructureTransactions(); - biomeLoader = registerLoader(IrisBiome.class); - dimensionLoader = registerLoader(IrisDimension.class); + biomeLoader = registerLoader(IrisBiome.class, replacement); + dimensionLoader = registerLoader(IrisDimension.class, replacement); builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter); gson = builder.create(); + loaders = replacement; if (biomeLoader == null || dimensionLoader == null) { throw new IllegalStateException("Unable to initialize Iris datapack compiler loaders for " + dataFolder); } diff --git a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java index fc79c41ff..25a2c14f3 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java @@ -31,8 +31,6 @@ import java.io.IOException; import java.util.HashSet; public class MatterObjectResourceLoader extends ResourceLoader { - private String[] possibleKeys; - public MatterObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName, Options options) { super(root, idm, folderName, resourceTypeName, IrisMatterObject.class, options); loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize()); @@ -67,7 +65,7 @@ public class MatterObjectResourceLoader extends ResourceLoader } } - private void findMatFiles(File dir, KSet m, HashSet visitedDirectories) { + private void findMatFiles(File dir, String prefix, KSet m, HashSet visitedDirectories) { if (dir == null || !dir.exists()) { return; } @@ -86,9 +84,9 @@ public class MatterObjectResourceLoader extends ResourceLoader for (File file : listedFiles) { if (file.isFile() && file.getName().endsWith(".mat")) { - m.add(file.getName().replaceAll("\\Q.mat\\E", "")); + m.add(prefix + file.getName().replaceAll("\\Q.mat\\E", "")); } else if (file.isDirectory()) { - findMatFiles(file, m, visitedDirectories); + findMatFiles(file, prefix + file.getName() + "/", m, visitedDirectories); } } } @@ -103,7 +101,7 @@ public class MatterObjectResourceLoader extends ResourceLoader HashSet visitedDirectories = new HashSet<>(); for (File folder : getFolders()) { - findMatFiles(folder, m, visitedDirectories); + findMatFiles(folder, "", m, visitedDirectories); } KList v = new KList<>(m); @@ -119,40 +117,6 @@ public class MatterObjectResourceLoader extends ResourceLoader } } - -// public String[] getPossibleKeys() { -// if (possibleKeys != null) { -// return possibleKeys; -// } -// -// IrisLogging.debug("Building " + resourceTypeName + " Possibility Lists"); -// KSet m = new KSet<>(); -// -// for (File i : getFolders()) { -// for (File j : i.listFiles()) { -// if (j.isFile() && j.getName().endsWith(".mat")) { -// m.add(j.getName().replaceAll("\\Q.mat\\E", "")); -// } else if (j.isDirectory()) { -// for (File k : j.listFiles()) { -// if (k.isFile() && k.getName().endsWith(".mat")) { -// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.mat\\E", "")); -// } else if (k.isDirectory()) { -// for (File l : k.listFiles()) { -// if (l.isFile() && l.getName().endsWith(".mat")) { -// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.mat\\E", "")); -// } -// } -// } -// } -// } -// } -// } -// -// KList v = new KList<>(m); -// possibleKeys = v.toArray(new String[0]); -// return possibleKeys; -// } - public File findFile(String name) { if (name == null || name.trim().isEmpty()) { return null; @@ -162,18 +126,10 @@ public class MatterObjectResourceLoader extends ResourceLoader return null; } - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } + File file = resolveFile(name, ".mat"); - File file = new File(i, name + ".mat"); - - if (file.exists()) { - return file; - } + if (file != null) { + return file; } IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); @@ -186,18 +142,10 @@ public class MatterObjectResourceLoader extends ResourceLoader } private IrisMatterObject loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } + File file = resolveFile(name, ".mat"); - File file = new File(i, name + ".mat"); - - if (file.exists()) { - return loadFile(file, name); - } + if (file != null) { + return loadFile(file, name); } IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); diff --git a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java index bda8a42d4..9f7fc902e 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java @@ -131,18 +131,10 @@ public class ObjectResourceLoader extends ResourceLoader { return null; } - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } + File file = resolveFile(name, ".iob"); - File file = new File(i, name + ".iob"); - - if (file.exists()) { - return file; - } + if (file != null) { + return file; } IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); @@ -155,21 +147,8 @@ public class ObjectResourceLoader extends ResourceLoader { } private IrisObject loadRaw(String name) { - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".iob"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - return null; + File file = resolveFile(name, ".iob"); + return file == null ? null : loadFile(file, name); } public IrisObject load(String name, boolean warn) { diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index 7753d3bed..902c8859f 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -52,7 +52,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; +import java.util.Comparator; import java.util.HashSet; import java.util.Locale; import java.util.Objects; @@ -64,6 +68,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -83,7 +88,7 @@ public class ResourceLoader implements MeteredCache { private static final Set schemaBuildQueue = ConcurrentHashMap.newKeySet(); private static final AtomicBoolean schemaBuildExecutorRegistered = new AtomicBoolean(); protected final AtomicCache> folderCache; - protected KSet firstAccess; + protected volatile KSet firstAccess; protected File root; protected String folderName; protected String resourceTypeName; @@ -163,18 +168,10 @@ public class ResourceLoader implements MeteredCache { return null; } - for (File i : getFolders(name)) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return j; - } - } + File file = resolveFile(name, ".json"); - File file = new File(i, name + ".json"); - - if (file.exists()) { - return file; - } + if (file != null) { + return file; } IrisLogging.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); @@ -182,6 +179,57 @@ public class ResourceLoader implements MeteredCache { return null; } + /** + * Resolves a resource file by key. An exact name + extension hit always wins; + * only then is the dotted-prefix scan used (so plains.json beats plains.disabled.json). + */ + protected File resolveFile(String name, String extension) { + return resolveFile(name, extension, getFolders(name)); + } + + protected File resolveFile(String name, String extension, KList folders) { + if (folders == null) { + return null; + } + + for (File folder : folders) { + File exact = new File(folder, name + extension); + + if (exact.isFile()) { + return exact; + } + + File[] listed = folder.listFiles(); + + if (listed == null) { + continue; + } + + KList matches = new KList<>(); + + for (File candidate : listed) { + if (candidate.isFile() && candidate.getName().endsWith(extension) && candidate.getName().split("\\Q.\\E")[0].equals(name)) { + matches.add(candidate); + } + } + + if (matches.isEmpty()) { + continue; + } + + if (matches.size() > 1) { + matches.sort(Comparator.comparing(File::getName)); + IrisLogging.warn("Ambiguous " + resourceTypeName + " " + name + " in " + folder.getPath() + ": " + + matches.stream().map(File::getName).collect(Collectors.joining(", ")) + + " (using " + matches.get(0).getName() + ")"); + } + + return matches.get(0); + } + + return null; + } + protected static String describeName(String name) { if (name == null) return ""; if (name.isEmpty()) return ""; @@ -413,22 +461,8 @@ public class ResourceLoader implements MeteredCache { } private T loadRaw(String name) { - for (File i : getFolders(name)) { - //noinspection ConstantConditions - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) { - return loadFile(j, name); - } - } - - File file = new File(i, name + ".json"); - - if (file.exists()) { - return loadFile(file, name); - } - } - - return null; + File file = resolveFile(name, ".json"); + return file == null ? null : loadFile(file, name); } public T load(String name, boolean warn) { @@ -440,53 +474,90 @@ public class ResourceLoader implements MeteredCache { return null; } - var set = firstAccess; - if (set != null) firstAccess.add(name); + KSet set = firstAccess; + if (set != null) set.add(name); return loadCache.get(name); } - public void loadFirstAccess(Engine engine) throws IOException { + private File prefetchFile(Engine engine) { String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode()); - File file = IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch"); + return IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch"); + } + + public void loadFirstAccess(Engine engine) throws IOException { + File file = prefetchFile(engine); if (!file.exists()) { return; } - FileInputStream fin = new FileInputStream(file); - GZIPInputStream gzi = new GZIPInputStream(fin); - DataInputStream din = new DataInputStream(gzi); - int m = din.readInt(); KList s = new KList<>(); - for (int i = 0; i < m; i++) { - s.add(din.readUTF()); + try (FileInputStream fin = new FileInputStream(file); + GZIPInputStream gzi = new GZIPInputStream(fin); + DataInputStream din = new DataInputStream(gzi)) { + int m = din.readInt(); + + if (m < 0) { + throw new IOException("Bad prefetch count " + m); + } + + for (int i = 0; i < m; i++) { + s.add(din.readUTF()); + } + } catch (IOException e) { + IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + e.getMessage()); + + if (!file.delete()) { + IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath()); + } + + return; } - din.close(); IrisLogging.info("Loading " + s.size() + " prefetch " + getFolderName()); firstAccess = null; loadAllParallel(s); } public void saveFirstAccess(Engine engine) throws IOException { - if (firstAccess == null) return; - String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode()); - File file = IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch"); - file.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(file); - GZIPOutputStream gzo = new CustomOutputStream(fos, 9); - DataOutputStream dos = new DataOutputStream(gzo); - var set = firstAccess; - firstAccess = null; - dos.writeInt(set.size()); + KSet set = firstAccess; + if (set == null) return; + KList snapshot = new KList<>(set); + File file = prefetchFile(engine); + File parent = file.getParentFile(); - for (String i : set) { - dos.writeUTF(i); + if (parent == null) { + throw new IOException("Prefetch path has no parent: " + file.getPath()); } - dos.flush(); - dos.close(); + if (!parent.isDirectory() && !parent.mkdirs() && !parent.isDirectory()) { + throw new IOException("Couldn't create prefetch folder " + parent.getPath()); + } + + File temp = File.createTempFile(file.getName(), ".tmp", parent); + + try { + try (FileOutputStream fos = new FileOutputStream(temp); + GZIPOutputStream gzo = new CustomOutputStream(fos, 9); + DataOutputStream dos = new DataOutputStream(gzo)) { + dos.writeInt(snapshot.size()); + + for (String i : snapshot) { + dos.writeUTF(i); + } + } + + try { + Files.move(temp.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temp.toPath()); + } + + firstAccess = null; } public KList getFolders() { @@ -531,21 +602,7 @@ public class ResourceLoader implements MeteredCache { } public File fileFor(T b) { - for (File i : getFolders()) { - for (File j : i.listFiles()) { - if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(b.getLoadKey())) { - return j; - } - } - - File file = new File(i, b.getLoadKey() + ".json"); - - if (file.exists()) { - return file; - } - } - - return null; + return resolveFile(b.getLoadKey(), ".json", getFolders()); } public boolean isLoaded(String next) { diff --git a/core/src/main/java/art/arcane/iris/core/nms/container/AutoClosing.java b/core/src/main/java/art/arcane/iris/core/nms/container/AutoClosing.java deleted file mode 100644 index 807fac52f..000000000 --- a/core/src/main/java/art/arcane/iris/core/nms/container/AutoClosing.java +++ /dev/null @@ -1,39 +0,0 @@ -package art.arcane.iris.core.nms.container; - -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.function.NastyRunnable; -import lombok.AllArgsConstructor; - -import java.util.concurrent.atomic.AtomicBoolean; - -@AllArgsConstructor -public class AutoClosing implements AutoCloseable { - private static final KMap CONTEXTS = new KMap<>(); - private final AtomicBoolean closed = new AtomicBoolean(); - private final NastyRunnable action; - - @Override - public void close() { - if (closed.getAndSet(true)) return; - try { - removeContext(); - action.run(); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - public void storeContext() { - CONTEXTS.put(Thread.currentThread(), this); - } - - public void removeContext() { - CONTEXTS.values().removeIf(c -> c == this); - } - - public static void closeContext() { - AutoClosing closing = CONTEXTS.remove(Thread.currentThread()); - if (closing == null) return; - closing.close(); - } -} diff --git a/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java index 92d891717..08f89e587 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/ContentKeyValidator.java @@ -18,8 +18,17 @@ package art.arcane.iris.core.pack; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -28,6 +37,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; public final class ContentKeyValidator { private static final int MAX_SUGGESTION_SCANS = 4096; @@ -221,4 +231,151 @@ public final class ContentKeyValidator { } return prev[lb]; } + + static void runContentKeyValidation(File packFolder, List warnings) { + try { + if (!IrisPlatforms.isBound()) { + return; + } + PlatformRegistries registries = IrisPlatforms.get().registries(); + if (registries == null) { + return; + } + List blockKeys = registries.blockKeys(); + List itemKeys = registries.itemKeys(); + List entityKeys = registries.entityKeys(); + if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) { + return; + } + + ReferencedContentKeys referenced = collectReferencedContentKeys(packFolder); + List errors = ContentKeyValidator.validate( + registries, referenced.blocks(), referenced.items(), referenced.entities()); + for (ContentKeyValidator.ContentKeyError error : errors) { + warnings.add(error.message()); + } + } catch (Throwable e) { + IrisLogging.reportError("PackValidator content-key validation failed for pack '" + packFolder.getName() + "'", e); + } + } + + private static ReferencedContentKeys collectReferencedContentKeys(File packFolder) { + Set blocks = new HashSet<>(); + Set items = new HashSet<>(); + Set entities = new HashSet<>(); + Set customBlocks = deriveRegistrantKeys(new File(packFolder, "blocks")); + + try (Stream stream = Files.walk(packFolder.toPath())) { + List files = stream.filter(Files::isRegularFile) + .filter(PackValidationIo::isScannableJsonPath) + .toList(); + for (Path path : files) { + String relative = packFolder.toPath().relativize(path).toString().replace(File.separatorChar, '/'); + boolean inLoot = relative.startsWith("loot/"); + boolean inEntities = relative.startsWith("entities/"); + JSONObject json; + try { + json = new JSONObject(Files.readString(path, StandardCharsets.UTF_8)); + } catch (Throwable ignored) { + continue; + } + collectFromNode(json, blocks, inLoot ? items : null, inEntities ? entities : null, customBlocks); + } + } catch (Throwable e) { + IrisLogging.reportError("PackValidator failed to walk pack for content-key extraction", e); + } + + return new ReferencedContentKeys(blocks, items, entities); + } + + private static void collectFromNode(Object node, Set blocks, Set items, Set entities, Set customBlocks) { + if (node instanceof JSONObject obj) { + for (String key : obj.keySet()) { + Object value = obj.get(key); + if (value instanceof String str) { + if ("block".equals(key)) { + addBlockRef(str, blocks, customBlocks); + } else if (items != null && "type".equals(key)) { + addSimpleRef(str, items); + } else if (entities != null && "type".equals(key)) { + addSimpleRef(str, entities); + } + } else { + collectFromNode(value, blocks, items, entities, customBlocks); + } + } + } else if (node instanceof JSONArray arr) { + for (int i = 0; i < arr.length(); i++) { + collectFromNode(arr.get(i), blocks, items, entities, customBlocks); + } + } + } + + private static void addBlockRef(String raw, Set blocks, Set customBlocks) { + String value = raw.trim().toLowerCase(Locale.ROOT); + int bracket = value.indexOf('['); + if (bracket >= 0) { + value = value.substring(0, bracket).trim(); + } + if (value.isEmpty() || customBlocks.contains(value)) { + return; + } + blocks.add(value); + } + + private static void addSimpleRef(String raw, Set target) { + String value = raw.trim().toLowerCase(Locale.ROOT); + if (!value.isEmpty()) { + target.add(value); + } + } + + private static Set deriveRegistrantKeys(File folder) { + Set keys = new HashSet<>(); + if (!folder.isDirectory()) { + return keys; + } + for (File file : PackValidationIo.listJsonRecursive(folder)) { + String key = PackValidationIo.deriveKey(folder, file); + if (key != null && !key.isBlank()) { + keys.add(key.toLowerCase(Locale.ROOT)); + } + } + return keys; + } + + static Set deriveRegistrantKeysExact(File folder) { + Set keys = new HashSet<>(); + if (!folder.isDirectory()) { + return keys; + } + for (File file : PackValidationIo.listJsonRecursive(folder)) { + String key = PackValidationIo.deriveKey(folder, file); + if (key != null && !key.isBlank()) { + keys.add(key); + } + } + return keys; + } + + static Set deriveObjectKeysExact(File folder) { + Set keys = new HashSet<>(); + if (!folder.isDirectory()) { + return keys; + } + try (Stream stream = Files.walk(folder.toPath())) { + stream.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".iob")) + .forEach(path -> { + Path relative = folder.toPath().relativize(path); + String key = relative.toString().replace(File.separatorChar, '/'); + keys.add(key.substring(0, key.length() - ".iob".length())); + }); + } catch (IOException ignored) { + } + return keys; + } + + private record ReferencedContentKeys(Set blocks, Set items, Set entities) { + } } diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java new file mode 100644 index 000000000..758a9a48a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDimensionValidator.java @@ -0,0 +1,193 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +final class PackDimensionValidator { + private PackDimensionValidator() { + } + + static void validateDimensions(File packFolder, File[] dimensionFiles, List blockingErrors, List warnings) { + File regionsFolder = new File(packFolder, "regions"); + File biomesFolder = new File(packFolder, "biomes"); + + for (File dimFile : dimensionFiles) { + String dimensionKey = PackValidationIo.stripExtension(dimFile.getName()); + JSONObject dimJson; + try { + dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors); + + JSONArray regionsArray = dimJson.optJSONArray("regions"); + if (regionsArray == null || regionsArray.length() == 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions."); + continue; + } + + int resolvedRegions = 0; + for (int i = 0; i < regionsArray.length(); i++) { + String regionKey = regionsArray.optString(i, null); + if (regionKey == null || regionKey.isBlank()) { + warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + "."); + continue; + } + File regionFile = new File(regionsFolder, regionKey + ".json"); + if (!regionFile.isFile()) { + blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'."); + continue; + } + + JSONObject regionJson; + try { + regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings); + if (anyBiome == 0) { + blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes."); + } + resolvedRegions++; + } + + if (resolvedRegions == 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions."); + } + } + } + + static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension, + List blockingErrors) { + if (!dimension.has("importedStructures")) { + return; + } + if (dimension.isNull("importedStructures")) { + blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object."); + return; + } + JSONObject policy = dimension.optJSONObject("importedStructures"); + if (policy == null) { + blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object."); + return; + } + if (policy.has("mode")) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.mode is not supported. Native structures are enabled by default; list explicit denials in importedStructures.disabled."); + } + if (policy.has("enabled")) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.enabled is not supported. Native structures are enabled by default; list explicit denials in importedStructures.disabled."); + } + validateStructureKeyList(dimensionKey, policy, "disabled", blockingErrors); + JSONArray adjustments = policy.optJSONArray("adjustments"); + if (adjustments == null) { + if (policy.has("adjustments")) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.adjustments must be an array."); + } + return; + } + for (int index = 0; index < adjustments.length(); index++) { + JSONObject adjustment = adjustments.optJSONObject(index); + if (adjustment == null) { + blockingErrors.add("Dimension '" + dimensionKey + + "' importedStructures.adjustments has a non-object entry at index " + index + "."); + continue; + } + validateStructureKeyList(dimensionKey, adjustment, "match", blockingErrors); + validateAdjustmentYBand(dimensionKey, adjustment, index, blockingErrors); + PackStructurePlacementValidator.validateNativeTerrain("Dimension '" + dimensionKey + + "' importedStructures.adjustments[" + index + "]", adjustment, blockingErrors); + } + } + + private static void validateAdjustmentYBand(String dimensionKey, JSONObject adjustment, int index, + List blockingErrors) { + if (!adjustment.has("yBand") || adjustment.opt("yBand") == JSONObject.NULL) { + return; + } + String path = "Dimension '" + dimensionKey + + "' importedStructures.adjustments[" + index + "].yBand"; + JSONObject band = adjustment.optJSONObject("yBand"); + if (band == null) { + blockingErrors.add(path + " must be an object."); + return; + } + PackJsonFieldChecks.validateOptionalIntegerRange(path, band, "min", -4064, 4064, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, band, "max", -4064, 4064, blockingErrors); + } + + private static void validateStructureKeyList(String dimensionKey, JSONObject owner, String field, + List blockingErrors) { + if (!owner.has(field)) { + return; + } + JSONArray keys = owner.optJSONArray(field); + if (keys == null) { + blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '" + + field + "' must be an array."); + return; + } + for (int index = 0; index < keys.length(); index++) { + Object value = keys.opt(index); + if (!(value instanceof String key) || key.isBlank()) { + blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '" + + field + "' has a blank or non-string entry at index " + index + "."); + } + } + } + + private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List warnings) { + JSONArray arr = regionJson.optJSONArray(field); + if (arr == null) { + return 0; + } + int resolved = 0; + for (int i = 0; i < arr.length(); i++) { + String biomeKey = arr.optString(i, null); + if (biomeKey == null || biomeKey.isBlank()) { + continue; + } + File biomeFile = new File(biomesFolder, biomeKey + ".json"); + if (!biomeFile.isFile()) { + warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + "."); + continue; + } + resolved++; + } + return resolved; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java b/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java index 2ad1a64be..a5ef9d11c 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackDownloader.java @@ -72,8 +72,9 @@ public final class PackDownloader { ZipUtil.unpack(zip, work); } catch (Throwable e) { IrisLogging.reportError(e); - e.printStackTrace(); feedback.accept(IrisLanguage.plain(PackDownloadMessages.UNPACK_FAILED)); + IO.delete(work); + return null; } File dir = null; File[] zipFiles = work.listFiles(); diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java b/core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java new file mode 100644 index 000000000..c397a5890 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackJsonFieldChecks.java @@ -0,0 +1,101 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONObject; + +import java.util.List; +import java.util.Set; + +final class PackJsonFieldChecks { + private PackJsonFieldChecks() { + } + + static void validateOptionalBoolean(String path, JSONObject object, String field, + List blockingErrors) { + if (!object.has(field) || object.opt(field) == JSONObject.NULL) { + return; + } + if (!(object.opt(field) instanceof Boolean)) { + blockingErrors.add(path + "." + field + " must be a boolean."); + } + } + + static void validateOptionalResourceKey(String path, JSONObject object, String field, + boolean allowNone, List blockingErrors) { + if (!object.has(field)) { + return; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof String value)) { + blockingErrors.add(path + "." + field + " must be a string."); + return; + } + String normalized = value.trim(); + if (normalized.isEmpty() || allowNone && "NONE".equalsIgnoreCase(normalized)) { + return; + } + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(normalized).matches()) { + blockingErrors.add(path + "." + field + " must be a namespaced registry key."); + } + } + + static void validateOptionalIntegerRange(String path, JSONObject object, String field, + int minimum, int maximum, + List blockingErrors) { + if (!object.has(field) || object.opt(field) == JSONObject.NULL) { + return; + } + Integer value = PackLootValidator.lootInteger(object, field, minimum, path, blockingErrors); + PackLootValidator.requireMinimum(path + "." + field, value, minimum, blockingErrors); + PackLootValidator.requireMaximum(path + "." + field, value, maximum, blockingErrors); + } + + static void validateOptionalDoubleRange(String path, JSONObject object, String field, + double minimum, double maximum, + List blockingErrors) { + if (!object.has(field) || object.opt(field) == JSONObject.NULL) { + return; + } + String fieldPath = path + "." + field; + Object rawValue = object.opt(field); + if (!(rawValue instanceof Number number) || !Double.isFinite(number.doubleValue())) { + blockingErrors.add(fieldPath + " must be a number."); + return; + } + double value = number.doubleValue(); + if (value < minimum) { + blockingErrors.add(fieldPath + " must be at least " + minimum + "."); + } + if (value > maximum) { + blockingErrors.add(fieldPath + " must be at most " + maximum + "."); + } + } + + static void validateOptionalEnum(String path, JSONObject object, String field, + Set values, List blockingErrors) { + if (!object.has(field)) { + return; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof String value) || !values.contains(value)) { + blockingErrors.add(path + "." + field + " must be one of " + values + "."); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java new file mode 100644 index 000000000..d324a1d75 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackLootValidator.java @@ -0,0 +1,240 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.IrisLoot; +import art.arcane.iris.engine.object.IrisLootReference; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +final class PackLootValidator { + private PackLootValidator() { + } + + static List validateLootGraph(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + File lootFolder = new File(packFolder, PackValidator.LOOT_FOLDER); + Set lootKeys = ContentKeyValidator.deriveRegistrantKeysExact(lootFolder); + if (lootFolder.isDirectory()) { + List lootFiles = PackValidationIo.listJsonRecursive(lootFolder); + lootFiles.sort(Comparator.comparing(File::getPath)); + for (File lootFile : lootFiles) { + String lootKey = PackValidationIo.deriveKey(lootFolder, lootFile); + JSONObject table = PackStructurePlacementValidator.readGraphJson( + lootFile, "Loot table", lootKey, blockingErrors); + if (table != null) { + validateLootTable(lootKey, table, blockingErrors); + } + } + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null || !resource.has("loot")) { + continue; + } + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors); + } + } + return blockingErrors; + } + + private static void validateLootTable(String lootKey, JSONObject table, List blockingErrors) { + String path = "Loot table '" + lootKey + "'"; + Integer rarity = lootInteger(table, "rarity", 1, path, blockingErrors); + Integer minimumPicked = lootInteger(table, "minPicked", 1, path, blockingErrors); + Integer maximumPicked = lootInteger(table, "maxPicked", 5, path, blockingErrors); + Integer maximumTries = lootInteger(table, "maxTries", 10, path, blockingErrors); + requireMinimum(path + ".rarity", rarity, 1, blockingErrors); + requireMinimum(path + ".minPicked", minimumPicked, 0, blockingErrors); + requireMinimum(path + ".maxPicked", maximumPicked, 1, blockingErrors); + requireMinimum(path + ".maxTries", maximumTries, 1, blockingErrors); + requireMaximum(path + ".minPicked", minimumPicked, IrisLootTable.MAX_PICKED, blockingErrors); + requireMaximum(path + ".maxPicked", maximumPicked, IrisLootTable.MAX_PICKED, blockingErrors); + requireMaximum(path + ".maxTries", maximumTries, IrisLootTable.MAX_TRIES, blockingErrors); + requireOrdered(path + ".minPicked", minimumPicked, path + ".maxPicked", maximumPicked, blockingErrors); + + JSONArray entries = table.optJSONArray("loot"); + if (entries == null || entries.length() == 0) { + blockingErrors.add(path + ".loot must be a non-empty array."); + return; + } + for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) { + JSONObject entry = entries.optJSONObject(entryIndex); + String entryPath = path + ".loot[" + entryIndex + "]"; + if (entry == null) { + blockingErrors.add(entryPath + " must be an object."); + continue; + } + String type = entry.optString("type", "").trim(); + if (type.isEmpty()) { + blockingErrors.add(entryPath + ".type must not be blank."); + } + Integer entryRarity = lootInteger(entry, "rarity", 1, entryPath, blockingErrors); + Integer minimumAmount = lootInteger(entry, "minAmount", 1, entryPath, blockingErrors); + Integer maximumAmount = lootInteger(entry, "maxAmount", 1, entryPath, blockingErrors); + requireMinimum(entryPath + ".rarity", entryRarity, 1, blockingErrors); + requireMinimum(entryPath + ".minAmount", minimumAmount, 1, blockingErrors); + requireMinimum(entryPath + ".maxAmount", maximumAmount, 1, blockingErrors); + requireMaximum(entryPath + ".minAmount", minimumAmount, IrisLoot.MAX_AMOUNT, blockingErrors); + requireMaximum(entryPath + ".maxAmount", maximumAmount, IrisLoot.MAX_AMOUNT, blockingErrors); + requireOrdered(entryPath + ".minAmount", minimumAmount, + entryPath + ".maxAmount", maximumAmount, blockingErrors); + validateLootEnchantments(entryPath, entry.opt("enchantments"), blockingErrors); + } + } + + private static void validateLootEnchantments(String entryPath, Object rawEnchantments, + List blockingErrors) { + if (rawEnchantments == null || rawEnchantments == JSONObject.NULL) { + return; + } + if (!(rawEnchantments instanceof JSONArray enchantments)) { + blockingErrors.add(entryPath + ".enchantments must be an array."); + return; + } + for (int enchantmentIndex = 0; enchantmentIndex < enchantments.length(); enchantmentIndex++) { + JSONObject enchantment = enchantments.optJSONObject(enchantmentIndex); + String enchantmentPath = entryPath + ".enchantments[" + enchantmentIndex + "]"; + if (enchantment == null) { + blockingErrors.add(enchantmentPath + " must be an object."); + continue; + } + if (enchantment.optString("enchantment", "").isBlank()) { + blockingErrors.add(enchantmentPath + ".enchantment must not be blank."); + } + Integer minimumLevel = lootInteger(enchantment, "minLevel", 1, enchantmentPath, blockingErrors); + Integer maximumLevel = lootInteger(enchantment, "maxLevel", 1, enchantmentPath, blockingErrors); + requireMinimum(enchantmentPath + ".minLevel", minimumLevel, 1, blockingErrors); + requireMinimum(enchantmentPath + ".maxLevel", maximumLevel, 1, blockingErrors); + requireOrdered(enchantmentPath + ".minLevel", minimumLevel, + enchantmentPath + ".maxLevel", maximumLevel, blockingErrors); + if (enchantment.has("chance")) { + Object rawChance = enchantment.opt("chance"); + if (!(rawChance instanceof Number number) + || !Double.isFinite(number.doubleValue()) + || number.doubleValue() < 0D + || number.doubleValue() > 1D) { + blockingErrors.add(enchantmentPath + ".chance must be a finite number from 0 to 1."); + } + } + } + } + + private static void validateLootReference(String resourceType, String resourceKey, Object rawLoot, + Set lootKeys, List blockingErrors) { + String path = resourceType + " '" + resourceKey + "'.loot"; + if (!(rawLoot instanceof JSONObject reference)) { + blockingErrors.add(path + " must be an object."); + return; + } + if (reference.has("mode")) { + Object rawMode = reference.opt("mode"); + if (!(rawMode instanceof String mode) + || !Set.of("ADD", "CLEAR", "REPLACE", "FALLBACK").contains(mode)) { + blockingErrors.add(path + ".mode must be ADD, CLEAR, REPLACE, or FALLBACK."); + } + } + if (reference.has("multiplier")) { + Object rawMultiplier = reference.opt("multiplier"); + if (!(rawMultiplier instanceof Number multiplier) + || !Double.isFinite(multiplier.doubleValue()) + || multiplier.doubleValue() < 0D + || multiplier.doubleValue() > IrisLootReference.MAX_MULTIPLIER) { + blockingErrors.add(path + ".multiplier must be a finite number from 0 to " + + (int) IrisLootReference.MAX_MULTIPLIER + "."); + } + } + if (!reference.has("tables")) { + return; + } + JSONArray tables = reference.optJSONArray("tables"); + if (tables == null) { + blockingErrors.add(path + ".tables must be an array."); + return; + } + for (int tableIndex = 0; tableIndex < tables.length(); tableIndex++) { + Object rawTableKey = tables.opt(tableIndex); + if (!(rawTableKey instanceof String tableKey) || tableKey.isBlank()) { + blockingErrors.add(path + ".tables[" + tableIndex + "] must name a loot table."); + } else if (!lootKeys.contains(tableKey)) { + blockingErrors.add(path + ".tables[" + tableIndex + + "] references missing loot table '" + tableKey + "'."); + } + } + } + + static Integer lootInteger(JSONObject object, String field, int defaultValue, + String path, List blockingErrors) { + if (!object.has(field)) { + return defaultValue; + } + Object rawValue = object.opt(field); + if (!(rawValue instanceof Number number) + || !Double.isFinite(number.doubleValue()) + || number.doubleValue() != Math.rint(number.doubleValue()) + || number.longValue() < Integer.MIN_VALUE + || number.longValue() > Integer.MAX_VALUE) { + blockingErrors.add(path + "." + field + " must be an integer."); + return null; + } + return number.intValue(); + } + + static void requireMinimum(String fieldPath, Integer value, int minimum, + List blockingErrors) { + if (value != null && value < minimum) { + blockingErrors.add(fieldPath + " must be at least " + minimum + "."); + } + } + + static void requireMaximum(String fieldPath, Integer value, int maximum, + List blockingErrors) { + if (value != null && value > maximum) { + blockingErrors.add(fieldPath + " must be at most " + maximum + "."); + } + } + + static void requireOrdered(String minimumPath, Integer minimum, String maximumPath, + Integer maximum, List blockingErrors) { + if (minimum != null && maximum != null && minimum > maximum) { + blockingErrors.add(minimumPath + " must not exceed " + maximumPath + "."); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java new file mode 100644 index 000000000..85dcc62c2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackNativeStructureValidator.java @@ -0,0 +1,341 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class PackNativeStructureValidator { + private PackNativeStructureValidator() { + } + + static List validateNativeStructureReplacements( + File packFolder, + Set replacementOutputStructures, + Map> sampledVerticalEnvelopes + ) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + Set viableStructures = replacementOutputStructures == null + ? Set.of() : replacementOutputStructures; + Map> verticalEnvelopes = + sampledVerticalEnvelopes == null ? Map.of() : sampledVerticalEnvelopes; + File structuresFolder = new File(packFolder, PackValidator.STRUCTURES_FOLDER); + Map structures = new HashMap<>(); + for (File structureFile : PackValidationIo.listJsonRecursive(structuresFolder)) { + JSONObject structure = PackValidationIo.readJson(structureFile); + if (structure != null) { + structures.put(PackValidationIo.deriveKey(structuresFolder, structureFile), structure); + } + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null || !placement.has("nativeSuppression")) { + continue; + } + Object rawSuppression = placement.opt("nativeSuppression"); + if (!(rawSuppression instanceof String suppression)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "].nativeSuppression must be NONE or REPLACE_SOURCE."); + continue; + } + if ("NONE".equals(suppression)) { + continue; + } + if (!"REPLACE_SOURCE".equals(suppression)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "].nativeSuppression has unsupported value '" + + suppression + "'. Use NONE or REPLACE_SOURCE."); + continue; + } + if (!PackValidator.DIMENSIONS_FOLDER.equals(folderName)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "] requests REPLACE_SOURCE, but native replacement is only" + + " valid on dimension-level placements."); + continue; + } + JSONArray references = placement.optJSONArray("structures"); + JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); + if (nativeStructures != null && nativeStructures.length() > 0) { + continue; + } + if (references == null || references.length() == 0) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] requests REPLACE_SOURCE without any structure backend."); + continue; + } + for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { + Object rawReference = references.opt(referenceIndex); + if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "].structures[" + referenceIndex + + "] must name an Iris structure for REPLACE_SOURCE."); + continue; + } + JSONObject structure = structures.get(structureKey); + if (structure == null) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] cannot REPLACE_SOURCE with missing or invalid structure '" + + structureKey + "'."); + continue; + } + String vanillaSource = structure.optString("vanillaSource", "").trim(); + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(vanillaSource).matches()) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] requests REPLACE_SOURCE for structure '" + structureKey + + "', but its vanillaSource is not a valid namespaced registry key."); + } + if (!viableStructures.contains(structureKey)) { + blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex + + "] requests REPLACE_SOURCE for structure '" + structureKey + + "', but that structure is not runtime-viable. Native generation will not" + + " be used as a fallback."); + continue; + } + validateReplacementVerticalEnvelope( + resourceKey, + resource, + placementIndex, + placement, + structureKey, + structure, + verticalEnvelopes.get(structureKey), + blockingErrors); + } + } + } + } + return blockingErrors; + } + + private static void validateReplacementVerticalEnvelope( + String dimensionKey, + JSONObject dimension, + int placementIndex, + JSONObject placement, + String structureKey, + JSONObject structure, + List sampledVerticalEnvelopes, + List blockingErrors + ) { + String context = "Dimension '" + dimensionKey + "' structures[" + placementIndex + + "] REPLACE_SOURCE structure '" + structureKey + "'"; + if (sampledVerticalEnvelopes == null || sampledVerticalEnvelopes.isEmpty()) { + blockingErrors.add(context + " has no sampled vertical envelope. Native generation will not" + + " be used as a fallback."); + return; + } + + DimensionVerticalBounds worldBounds = resolveDimensionVerticalBounds(dimension, context, blockingErrors); + PlacementVerticalBounds placementBounds = resolvePlacementVerticalBounds(placement, context, blockingErrors); + ObjectPlaceMode placeMode = resolvePlaceMode(structure, context, blockingErrors); + if (worldBounds == null || placementBounds == null || placeMode == null) { + return; + } + + for (StructureGraphPackValidator.SampledVerticalEnvelope sampled : sampledVerticalEnvelopes) { + boolean exactY = placementBounds.underground() + || sampled.pieceCount() > 1 + || placeMode == ObjectPlaceMode.STRUCTURE_PIECE + || placeMode == ObjectPlaceMode.FLOATING; + if (!exactY) { + continue; + } + + long minimumYOffset = sampled.minimumYOffset(); + long maximumYOffset = sampled.maximumYOffset(); + boolean surfaceAligned = !placementBounds.underground() + && sampled.pieceCount() > 1 + && placeMode != ObjectPlaceMode.STRUCTURE_PIECE + && placeMode != ObjectPlaceMode.FLOATING; + if (surfaceAligned) { + maximumYOffset -= minimumYOffset; + minimumYOffset = 0L; + } + + boolean fitsConfiguredRange; + if (placementBounds.underground()) { + long minimumAnchor = Math.max( + Math.max(placementBounds.minimumY(), worldBounds.minimumY()), + worldBounds.minimumY() - minimumYOffset); + long maximumAnchor = Math.min( + Math.min(placementBounds.maximumY(), worldBounds.maximumY()), + worldBounds.maximumY() - maximumYOffset); + fitsConfiguredRange = minimumAnchor <= maximumAnchor; + } else { + long minimumTerrainY = Math.max(placementBounds.minimumY(), worldBounds.minimumY()); + long maximumTerrainY = Math.min(placementBounds.maximumY(), worldBounds.maximumY()); + fitsConfiguredRange = minimumTerrainY <= maximumTerrainY + && minimumTerrainY + minimumYOffset >= worldBounds.minimumY() + && maximumTerrainY + maximumYOffset <= worldBounds.maximumY(); + } + if (fitsConfiguredRange) { + continue; + } + + String alignment = surfaceAligned ? "surface-aligned " : ""; + blockingErrors.add(context + " sampled seed " + sampled.seed() + " has an " + alignment + + "exact-Y piece envelope " + minimumYOffset + ".." + maximumYOffset + + " relative to its anchor, which cannot fit placement band " + + placementBounds.minimumY() + ".." + placementBounds.maximumY() + + " inside writable world " + worldBounds.minimumY() + ".." + worldBounds.maximumY() + + ". Native generation will not be used as a fallback."); + return; + } + } + + private static DimensionVerticalBounds resolveDimensionVerticalBounds( + JSONObject dimension, + String context, + List blockingErrors + ) { + long dimensionMinimum = -64L; + long dimensionMaximum = 320L; + if (dimension.has("dimensionHeight")) { + JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight"); + if (dimensionHeight == null) { + blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight" + + " must be an object."); + return null; + } + Long configuredMinimum = integralJsonNumber(dimensionHeight, "min", 16L); + Long configuredMaximum = integralJsonNumber(dimensionHeight, "max", 32L); + if (configuredMinimum == null || configuredMaximum == null) { + blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight" + + " min and max must be finite integer values."); + return null; + } + dimensionMinimum = configuredMinimum; + dimensionMaximum = configuredMaximum; + } + + long writableMinimum = dimensionMinimum + 1L; + long writableMaximum = dimensionMaximum - 1L; + if (writableMinimum > writableMaximum) { + blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight " + + dimensionMinimum + ".." + dimensionMaximum + " has no writable structure range."); + return null; + } + return new DimensionVerticalBounds(writableMinimum, writableMaximum); + } + + private static PlacementVerticalBounds resolvePlacementVerticalBounds( + JSONObject placement, + String context, + List blockingErrors + ) { + boolean underground = false; + if (placement.has("underground")) { + Object rawUnderground = placement.opt("underground"); + if (!(rawUnderground instanceof Boolean configuredUnderground)) { + blockingErrors.add(context + " cannot validate its vertical envelope because underground" + + " must be true or false."); + return null; + } + underground = configuredUnderground; + } + Long configuredMinimum = integralJsonNumber(placement, "minHeight", -2032L); + Long configuredMaximum = integralJsonNumber(placement, "maxHeight", 2032L); + if (configuredMinimum == null || configuredMaximum == null) { + blockingErrors.add(context + " cannot validate its vertical envelope because minHeight and" + + " maxHeight must be finite integer values."); + return null; + } + long minimumY = underground + ? Math.min(configuredMinimum, configuredMaximum) : configuredMinimum; + long maximumY = underground + ? Math.max(configuredMinimum, configuredMaximum) : configuredMaximum; + return new PlacementVerticalBounds(minimumY, maximumY, underground); + } + + private static ObjectPlaceMode resolvePlaceMode( + JSONObject structure, + String context, + List blockingErrors + ) { + if (!structure.has("placeMode")) { + return ObjectPlaceMode.STRUCTURE_PIECE; + } + Object rawPlaceMode = structure.opt("placeMode"); + if (!(rawPlaceMode instanceof String placeModeName)) { + blockingErrors.add(context + " cannot validate its vertical envelope because placeMode must name" + + " an Iris object place mode."); + return null; + } + try { + return ObjectPlaceMode.valueOf(placeModeName); + } catch (IllegalArgumentException exception) { + blockingErrors.add(context + " cannot validate its vertical envelope because placeMode '" + + placeModeName + "' is not supported."); + return null; + } + } + + private static Long integralJsonNumber(JSONObject owner, String field, long defaultValue) { + if (!owner.has(field)) { + return defaultValue; + } + Object rawValue = owner.opt(field); + if (!(rawValue instanceof Number number)) { + return null; + } + double value = number.doubleValue(); + if (!Double.isFinite(value) || value != Math.rint(value) + || value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { + return null; + } + return (long) value; + } + + private record DimensionVerticalBounds(long minimumY, long maximumY) { + } + + private record PlacementVerticalBounds(long minimumY, long maximumY, boolean underground) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java new file mode 100644 index 000000000..93edea8e8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackObjectSurfaceValidator.java @@ -0,0 +1,227 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +final class PackObjectSurfaceValidator { + private PackObjectSurfaceValidator() { + } + + static Set collectPlacedStructureKeys(File packFolder) { + Set structureKeys = new LinkedHashSet<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return structureKeys; + } + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + for (File resourceFile : PackValidationIo.listJsonRecursive(resourceFolder)) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null) { + continue; + } + JSONArray references = placement.optJSONArray("structures"); + if (references == null) { + continue; + } + for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { + Object rawReference = references.opt(referenceIndex); + if (rawReference instanceof String structureKey && !structureKey.isBlank()) { + structureKeys.add(structureKey); + } + } + } + } + } + return Set.copyOf(structureKeys); + } + + static List validateUnsupportedStructureTransforms(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + JSONObject resource = PackStructurePlacementValidator.readGraphJson( + resourceFile, resourceType, resourceKey, blockingErrors); + if (resource == null) { + continue; + } + + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null) { + continue; + } + for (String field : PackValidator.UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS) { + if (placement.has(field)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + placementIndex + + "] declares unsupported field '" + field + + "'. Structure placement transforms are not supported; remove the field."); + } + } + } + } + } + return blockingErrors; + } + + static List validateStructureGraph(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + File structuresFolder = new File(packFolder, PackValidator.STRUCTURES_FOLDER); + File poolsFolder = new File(packFolder, PackValidator.JIGSAW_POOLS_FOLDER); + File piecesFolder = new File(packFolder, PackValidator.JIGSAW_PIECES_FOLDER); + File objectsFolder = new File(packFolder, PackValidator.OBJECTS_FOLDER); + Set structureKeys = ContentKeyValidator.deriveRegistrantKeysExact(structuresFolder); + Set poolKeys = ContentKeyValidator.deriveRegistrantKeysExact(poolsFolder); + Set pieceKeys = ContentKeyValidator.deriveRegistrantKeysExact(piecesFolder); + Set objectKeys = ContentKeyValidator.deriveObjectKeysExact(objectsFolder); + + PackStructurePlacementValidator.validateStructurePlacements(packFolder, structureKeys, blockingErrors); + PackStructurePlacementValidator.validateStructureStartPools(structuresFolder, poolKeys, blockingErrors); + PackStructurePlacementValidator.validateJigsawPools(poolsFolder, poolKeys, pieceKeys, blockingErrors); + PackStructurePlacementValidator.validateJigsawPieces(piecesFolder, poolKeys, objectKeys, blockingErrors); + return blockingErrors; + } + + static List validateRemovedWorldgenFields(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + for (String field : PackValidator.REMOVED_WORLDGEN_FIELDS) { + if (resource.has(field)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' declares removed field '" + + field + "'. Remove it because fluid-body generation is not supported."); + } + } + } + } + return blockingErrors; + } + + static List validateObjectSurfaceSupport(File packFolder) { + List blockingErrors = new ArrayList<>(); + if (packFolder == null || !packFolder.isDirectory()) { + return blockingErrors; + } + + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = PackStructurePlacementValidator.structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + String path = resourceType + " '" + PackValidationIo.deriveKey(resourceFolder, resourceFile) + "'"; + if ("dimensions".equals(folderName)) { + PackJsonFieldChecks.validateOptionalIntegerRange( + path, resource, "objectSurfaceSupportBuffer", 0, 16, blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean( + path, resource, "requireObjectSurfaceSupport", blockingErrors); + } + validateObjectPlacementSurfaceSupport(path, resource.optJSONArray("objects"), blockingErrors); + } + } + return blockingErrors; + } + + private static void validateObjectPlacementSurfaceSupport(String path, JSONArray placements, + List blockingErrors) { + if (placements == null) { + return; + } + for (int i = 0; i < placements.length(); i++) { + JSONObject placement = placements.optJSONObject(i); + if (placement == null) { + continue; + } + String placementPath = path + ".objects[" + i + "]"; + if (placement.has("surfaceOpeningClearance")) { + blockingErrors.add(placementPath + " declares removed field 'surfaceOpeningClearance'. " + + "Use surfaceSupportBuffer instead."); + } + PackJsonFieldChecks.validateOptionalIntegerRange( + placementPath, placement, "surfaceSupportBuffer", 0, 16, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + placementPath, placement, "surfaceSupportDepth", 1, 16, blockingErrors); + PackJsonFieldChecks.validateOptionalBoolean( + placementPath, placement, "requireSurfaceSupport", blockingErrors); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java new file mode 100644 index 000000000..7e2d1f470 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackSpawnValidator.java @@ -0,0 +1,343 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformEntityType; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +final class PackSpawnValidator { + private PackSpawnValidator() { + } + + static List validateSpawnerEntityReferences(File spawnersFolder, File entitiesFolder) { + List blockingErrors = new ArrayList<>(); + if (spawnersFolder == null || !spawnersFolder.isDirectory()) { + return blockingErrors; + } + + Path entityRoot = entitiesFolder.toPath().toAbsolutePath().normalize(); + Set validEntityFiles = new HashSet<>(); + Map invalidEntityFiles = new HashMap<>(); + List spawnerFiles = PackValidationIo.listJsonRecursive(spawnersFolder); + spawnerFiles.sort(Comparator.comparing(File::getPath)); + for (File spawnerFile : spawnerFiles) { + String spawnerKey = PackValidationIo.deriveKey(spawnersFolder, spawnerFile); + JSONObject spawner; + try { + spawner = new JSONObject(Files.readString(spawnerFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Spawner '" + spawnerKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + validateSpawnerSpawnEntries(spawnerKey, spawner, "spawns", entityRoot, + validEntityFiles, invalidEntityFiles, blockingErrors); + validateSpawnerSpawnEntries(spawnerKey, spawner, "initialSpawns", entityRoot, + validEntityFiles, invalidEntityFiles, blockingErrors); + } + return blockingErrors; + } + + private static void validateSpawnerSpawnEntries(String spawnerKey, + JSONObject spawner, + String field, + Path entityRoot, + Set validEntityFiles, + Map invalidEntityFiles, + List blockingErrors) { + if (!spawner.has(field)) { + return; + } + JSONArray entries = spawner.optJSONArray(field); + if (entries == null) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " must be an array."); + return; + } + + for (int index = 0; index < entries.length(); index++) { + JSONObject entry = entries.optJSONObject(index); + if (entry == null) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " has a non-object entry at index " + index + "."); + continue; + } + if (!entry.has("entity") || entry.isNull("entity")) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " has an entry without an entity reference at index " + index + "."); + continue; + } + + Object rawEntity = entry.get("entity"); + if (!(rawEntity instanceof String entityKey)) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " entity reference at index " + index + " must be a string."); + continue; + } + if (entityKey.isBlank()) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + + " has a blank entity reference at index " + index + "."); + continue; + } + + Path entityFile; + try { + if (entityKey.indexOf('\\') >= 0) { + throw new IllegalArgumentException("backslash path separators are not portable"); + } + entityFile = entityRoot.resolve(entityKey + ".json").normalize(); + } catch (RuntimeException e) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " has invalid entity reference '" + entityKey + "': " + e.getMessage()); + continue; + } + if (!entityFile.startsWith(entityRoot)) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " has unsafe entity reference '" + entityKey + "'."); + continue; + } + if (!Files.isRegularFile(entityFile)) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " references missing entity '" + entityKey + "'."); + continue; + } + + String invalidJson = invalidEntityFiles.get(entityFile); + if (invalidJson != null) { + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " references malformed entity '" + entityKey + "': " + invalidJson); + continue; + } + if (validEntityFiles.contains(entityFile)) { + continue; + } + + try { + new JSONObject(Files.readString(entityFile, StandardCharsets.UTF_8)); + validEntityFiles.add(entityFile); + } catch (Throwable e) { + String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); + invalidEntityFiles.put(entityFile, message); + blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index + + " references malformed entity '" + entityKey + "': " + message); + } + } + } + + static List validateCustomBiomeSpawns(File biomesFolder, Function categoryResolver) { + List blockingErrors = new ArrayList<>(); + if (biomesFolder == null || !biomesFolder.isDirectory()) { + return blockingErrors; + } + + List biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder); + biomeFiles.sort(Comparator.comparing(File::getPath)); + for (File biomeFile : biomeFiles) { + String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); + JSONObject biome; + try { + biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Biome '" + biomeKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + JSONArray derivatives = biome.optJSONArray("customDerivitives"); + if (derivatives == null) { + if (biome.has("customDerivitives") && !biome.isNull("customDerivitives")) { + blockingErrors.add("Biome '" + biomeKey + "' customDerivitives must be an array."); + } + continue; + } + for (int derivativeIndex = 0; derivativeIndex < derivatives.length(); derivativeIndex++) { + JSONObject derivative = derivatives.optJSONObject(derivativeIndex); + if (derivative == null) { + blockingErrors.add("Biome '" + biomeKey + "' has a non-object custom derivative at index " + derivativeIndex + "."); + continue; + } + validateCustomBiomeDerivativeTags(biomeKey, derivative, derivativeIndex, blockingErrors); + validateCustomBiomeDerivativeSpawns( + biomeKey, derivative, derivativeIndex, categoryResolver, blockingErrors); + } + } + return blockingErrors; + } + + private static void validateCustomBiomeDerivativeTags(String biomeKey, + JSONObject derivative, + int derivativeIndex, + List blockingErrors) { + if (!derivative.has("tags") || derivative.isNull("tags")) { + return; + } + String derivativeId = derivative.optString("id", "#" + derivativeIndex); + JSONArray tags = derivative.optJSONArray("tags"); + if (tags == null) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' tags must be an array."); + return; + } + for (int tagIndex = 0; tagIndex < tags.length(); tagIndex++) { + Object rawTag = tags.opt(tagIndex); + if (!(rawTag instanceof String tag)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has a non-string tag at index " + tagIndex + "."); + continue; + } + String normalized = tag.trim().toLowerCase(Locale.ROOT); + if (normalized.indexOf(':') < 0) { + normalized = "minecraft:" + normalized; + } + if (!isSafeResourceKey(normalized)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has invalid tag '" + tag + "'."); + } + } + } + + private static boolean isSafeResourceKey(String key) { + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(key).matches()) { + return false; + } + int separator = key.indexOf(':'); + String[] segments = key.substring(separator + 1).split("/"); + for (String segment : segments) { + if (segment.equals("..")) { + return false; + } + } + return true; + } + + private static void validateCustomBiomeDerivativeSpawns(String biomeKey, + JSONObject derivative, + int derivativeIndex, + Function categoryResolver, + List blockingErrors) { + JSONArray spawns = derivative.optJSONArray("spawns"); + if (spawns == null) { + if (derivative.has("spawns") && !derivative.isNull("spawns")) { + String derivativeId = derivative.optString("id", "#" + derivativeIndex); + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawns must be an array."); + } + return; + } + String derivativeId = derivative.optString("id", "#" + derivativeIndex); + for (int spawnIndex = 0; spawnIndex < spawns.length(); spawnIndex++) { + JSONObject spawn = spawns.optJSONObject(spawnIndex); + if (spawn == null) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has a non-object spawn at index " + spawnIndex + "."); + continue; + } + + String type = spawn.optString("type", "").trim().toLowerCase(Locale.ROOT); + if (type.isEmpty()) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' has a spawn without an entity type at index " + spawnIndex + "."); + continue; + } + String typeKey = type.indexOf(':') >= 0 ? type : "minecraft:" + type; + SpawnCategoryResolution resolution; + try { + resolution = categoryResolver == null ? null : categoryResolver.apply(typeKey); + } catch (Throwable e) { + IrisLogging.reportError("PackValidator failed to resolve spawn category for '" + typeKey + "'", e); + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn category lookup failed for '" + typeKey + "': " + e.getMessage()); + continue; + } + if (resolution != null && !resolution.entityKnown()) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn references unknown entity type '" + typeKey + "'."); + continue; + } + String expectedGroup = resolution == null ? null : resolution.category(); + String group = spawn.optString("group", "").trim(); + if (group.isEmpty()) { + if (expectedGroup != null && !expectedGroup.isBlank() + && !IrisBiomeCustomSpawnType.MISC.name().equalsIgnoreCase(expectedGroup)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn '" + typeKey + "' must declare group '" + + expectedGroup.toUpperCase(Locale.ROOT) + "'."); + } + continue; + } + + IrisBiomeCustomSpawnType configuredGroup; + try { + configuredGroup = IrisBiomeCustomSpawnType.valueOf(group); + } catch (IllegalArgumentException e) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn '" + typeKey + "' declares unknown group '" + group + "'."); + continue; + } + + if (expectedGroup != null && !expectedGroup.isBlank() + && !configuredGroup.name().equalsIgnoreCase(expectedGroup)) { + blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId + + "' spawn '" + typeKey + "' declares group '" + configuredGroup.name() + + "' but the live entity registry requires '" + expectedGroup.toUpperCase(Locale.ROOT) + "'."); + } + } + } + + static SpawnCategoryResolution resolveEntitySpawnCategory(String typeKey) { + if (!IrisPlatforms.isBound()) { + return null; + } + PlatformRegistries registries = IrisPlatforms.get().registries(); + if (registries == null) { + return null; + } + PlatformEntityType entityType = registries.entity(typeKey); + return entityType == null + ? SpawnCategoryResolution.unknown() + : SpawnCategoryResolution.known(entityType.spawnCategory()); + } + + record SpawnCategoryResolution(boolean entityKnown, String category) { + static SpawnCategoryResolution unknown() { + return new SpawnCategoryResolution(false, null); + } + + static SpawnCategoryResolution known(String category) { + return new SpawnCategoryResolution(true, category); + } + } +} 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 new file mode 100644 index 000000000..1e16f5038 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackStructurePlacementValidator.java @@ -0,0 +1,389 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +final class PackStructurePlacementValidator { + private PackStructurePlacementValidator() { + } + + static void validateStructurePlacements(File packFolder, + Set structureKeys, + List blockingErrors) { + Set registeredStructures = registeredStructureKeys(); + Set registeredJigsaws = registeredJigsawKeys(); + Set registeredPools = registeredTemplatePoolKeys(); + for (String folderName : PackValidator.STRUCTURE_HOST_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + List resourceFiles = PackValidationIo.listJsonRecursive(resourceFolder); + resourceFiles.sort(Comparator.comparing(File::getPath)); + String resourceType = structureHostType(folderName); + for (File resourceFile : resourceFiles) { + JSONObject resource = PackValidationIo.readJson(resourceFile); + if (resource == null) { + continue; + } + JSONArray placements = resource.optJSONArray("structures"); + if (placements == null) { + continue; + } + String resourceKey = PackValidationIo.deriveKey(resourceFolder, resourceFile); + for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { + JSONObject placement = placements.optJSONObject(placementIndex); + if (placement == null) { + continue; + } + JSONArray references = placement.optJSONArray("structures"); + JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); + boolean hasIrisStructures = references != null && references.length() > 0; + boolean hasNativeStructures = nativeStructures != null && nativeStructures.length() > 0; + String placementPath = resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "]"; + if (hasIrisStructures == hasNativeStructures) { + blockingErrors.add(placementPath + + " must declare exactly one non-empty backend: structures or nativeStructures."); + continue; + } + if (hasNativeStructures) { + validateNativeStructures( + placementPath, placement, nativeStructures, + registeredStructures, registeredJigsaws, + registeredPools, blockingErrors); + continue; + } + for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { + Object rawReference = references.opt(referenceIndex); + if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) { + continue; + } + if (!structureKeys.contains(structureKey)) { + blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + + placementIndex + "].structures[" + referenceIndex + + "] references missing structure '" + structureKey + "'."); + } + } + } + } + } + } + + private static Set registeredJigsawKeys() { + try { + List registered = IrisPlatforms.get().structureHooks().jigsawStructureKeys(); + if (registered == null || registered.isEmpty()) { + return Set.of(); + } + Set keys = new HashSet<>(); + for (String key : registered) { + if (key != null && !key.isBlank()) { + keys.add(key.toLowerCase(Locale.ROOT)); + } + } + return Set.copyOf(keys); + } catch (Throwable ignored) { + return Set.of(); + } + } + + private static Set registeredStructureKeys() { + try { + List registered = IrisPlatforms.get().structureHooks().structureKeys(); + if (registered == null || registered.isEmpty()) { + return Set.of(); + } + Set keys = new HashSet<>(); + for (String key : registered) { + if (key != null && !key.isBlank()) { + keys.add(key.toLowerCase(Locale.ROOT)); + } + } + return Set.copyOf(keys); + } catch (Throwable ignored) { + return Set.of(); + } + } + + private static Set registeredTemplatePoolKeys() { + try { + List registered = IrisPlatforms.get().structureHooks().templatePoolKeys(); + if (registered == null || registered.isEmpty()) { + return Set.of(); + } + Set keys = new HashSet<>(); + for (String key : registered) { + if (key != null && !key.isBlank()) { + keys.add(key.toLowerCase(Locale.ROOT)); + } + } + return Set.copyOf(keys); + } catch (Throwable ignored) { + return Set.of(); + } + } + + private static void validateNativeStructures(String placementPath, JSONObject placement, + JSONArray nativeStructures, + Set registeredStructures, + Set registeredJigsaws, + Set registeredPools, + List blockingErrors) { + for (int sourceIndex = 0; sourceIndex < nativeStructures.length(); sourceIndex++) { + String sourcePath = placementPath + ".nativeStructures[" + sourceIndex + "]"; + JSONObject source = nativeStructures.optJSONObject(sourceIndex); + if (source == null) { + blockingErrors.add(sourcePath + " must be an object."); + continue; + } + String structureKey = source.optString("structure", "").trim(); + if (!PackValidator.RESOURCE_KEY_PATTERN.matcher(structureKey).matches()) { + blockingErrors.add(sourcePath + ".structure must be a namespaced registry key."); + } else if (!registeredStructures.isEmpty() + && !registeredStructures.contains(structureKey.toLowerCase(Locale.ROOT))) { + blockingErrors.add(sourcePath + ".structure '" + structureKey + + "' is not a registered structure."); + } + Integer weight = PackLootValidator.lootInteger(source, "weight", 1, sourcePath, blockingErrors); + PackLootValidator.requireMinimum(sourcePath + ".weight", weight, 1, blockingErrors); + JSONObject jigsaw = source.optJSONObject("jigsaw"); + if (source.has("jigsaw") && source.opt("jigsaw") != JSONObject.NULL && jigsaw == null) { + blockingErrors.add(sourcePath + ".jigsaw must be an object."); + } else if (jigsaw != null) { + if (!registeredJigsaws.isEmpty() + && !registeredJigsaws.contains(structureKey.toLowerCase(Locale.ROOT))) { + blockingErrors.add(sourcePath + + ".jigsaw requires a registered jigsaw structure."); + } + validateJigsawAssembly( + sourcePath + ".jigsaw", jigsaw, registeredPools, blockingErrors); + } + } + validateNativeTerrain(placementPath, placement, blockingErrors); + } + + private static void validateJigsawAssembly(String path, JSONObject assembly, + Set registeredPools, + List blockingErrors) { + PackJsonFieldChecks.validateOptionalResourceKey(path, assembly, "startPool", false, blockingErrors); + String startPool = assembly.optString("startPool", "").trim(); + if (!startPool.isEmpty() && !registeredPools.isEmpty() + && !registeredPools.contains(startPool.toLowerCase(Locale.ROOT))) { + blockingErrors.add(path + ".startPool '" + startPool + + "' is not a registered template pool."); + } + PackJsonFieldChecks.validateOptionalResourceKey(path, assembly, "startJigsawName", true, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDepth", 0, 20, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDistanceHorizontal", 1, 128, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path, assembly, "maxDistanceVertical", 1, 4064, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, assembly, "dimensionPaddingBottom", 0, Integer.MAX_VALUE, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange( + path, assembly, "dimensionPaddingTop", 0, Integer.MAX_VALUE, blockingErrors); + if (assembly.has("useExpansionHack") + && !(assembly.opt("useExpansionHack") instanceof Boolean)) { + blockingErrors.add(path + ".useExpansionHack must be a boolean."); + } + PackJsonFieldChecks.validateOptionalEnum(path, assembly, "projectStartToHeightmap", + Set.of("SOURCE", "NONE", "WORLD_SURFACE_WG", "WORLD_SURFACE", + "OCEAN_FLOOR_WG", "OCEAN_FLOOR", "MOTION_BLOCKING", + "MOTION_BLOCKING_NO_LEAVES"), blockingErrors); + PackJsonFieldChecks.validateOptionalEnum(path, assembly, "liquidSettings", + Set.of("SOURCE", "IGNORE_WATERLOGGING", "APPLY_WATERLOGGING"), blockingErrors); + } + + static void validateNativeTerrain(String path, JSONObject placement, + List blockingErrors) { + JSONObject terrain = placement.optJSONObject("terrain"); + if (placement.has("terrain") && placement.opt("terrain") != JSONObject.NULL && terrain == null) { + blockingErrors.add(path + ".terrain must be an object."); + return; + } + if (terrain == null) { + return; + } + PackJsonFieldChecks.validateOptionalEnum(path + ".terrain", terrain, "mode", + Set.of("SOURCE", "PRESERVE", "BORE", "FORCE_CARVE", "VACUUM", "ENCASE"), blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain, + "horizontalPadding", 0, 128, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain, + "ceilingPadding", 0, 128, blockingErrors); + PackJsonFieldChecks.validateOptionalIntegerRange(path + ".terrain", terrain, + "floorPadding", 0, 64, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "erosionStrength", 0D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "erosionFrequency", 0.001D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "lobeFrequency", 0D, 1D, blockingErrors); + PackJsonFieldChecks.validateOptionalDoubleRange(path + ".terrain", terrain, + "lobeStrength", 0D, 1D, blockingErrors); + if (terrain.has("encasePalette") && terrain.opt("encasePalette") != JSONObject.NULL + && terrain.optJSONObject("encasePalette") == null) { + blockingErrors.add(path + ".terrain.encasePalette must be an object."); + } + } + + static void validateStructureStartPools(File structuresFolder, + Set poolKeys, + List blockingErrors) { + if (!structuresFolder.isDirectory()) { + return; + } + List structureFiles = PackValidationIo.listJsonRecursive(structuresFolder); + structureFiles.sort(Comparator.comparing(File::getPath)); + for (File structureFile : structureFiles) { + String structureKey = PackValidationIo.deriveKey(structuresFolder, structureFile); + JSONObject structure = readGraphJson(structureFile, "Structure", structureKey, blockingErrors); + if (structure == null || isLegacyStructureIndex(structureKey, structure)) { + continue; + } + String startPool = structure.optString("startPool", "").trim(); + if (startPool.isEmpty()) { + blockingErrors.add("Structure '" + structureKey + "' does not declare a startPool."); + } else if (!poolKeys.contains(startPool)) { + blockingErrors.add("Structure '" + structureKey + "' references missing start pool '" + + startPool + "'."); + } + } + } + + static void validateJigsawPools(File poolsFolder, + Set poolKeys, + Set pieceKeys, + List blockingErrors) { + if (!poolsFolder.isDirectory()) { + return; + } + List poolFiles = PackValidationIo.listJsonRecursive(poolsFolder); + poolFiles.sort(Comparator.comparing(File::getPath)); + for (File poolFile : poolFiles) { + String poolKey = PackValidationIo.deriveKey(poolsFolder, poolFile); + JSONObject pool = readGraphJson(poolFile, "Jigsaw pool", poolKey, blockingErrors); + if (pool == null) { + continue; + } + JSONArray entries = pool.optJSONArray("pieces"); + if (entries != null) { + for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) { + JSONObject entry = entries.optJSONObject(entryIndex); + if (entry == null) { + continue; + } + String pieceKey = entry.optString("piece", "").trim(); + if (!pieceKey.isEmpty() && !pieceKeys.contains(pieceKey)) { + blockingErrors.add("Jigsaw pool '" + poolKey + "' pieces[" + entryIndex + + "] references missing piece '" + pieceKey + "'."); + } + } + } + String fallback = pool.optString("fallback", "").trim(); + if (!fallback.isEmpty() && !poolKeys.contains(fallback)) { + blockingErrors.add("Jigsaw pool '" + poolKey + "' references missing fallback pool '" + + fallback + "'."); + } + } + } + + static void validateJigsawPieces(File piecesFolder, + Set poolKeys, + Set objectKeys, + List blockingErrors) { + if (!piecesFolder.isDirectory()) { + return; + } + List pieceFiles = PackValidationIo.listJsonRecursive(piecesFolder); + pieceFiles.sort(Comparator.comparing(File::getPath)); + for (File pieceFile : pieceFiles) { + String pieceKey = PackValidationIo.deriveKey(piecesFolder, pieceFile); + JSONObject piece = readGraphJson(pieceFile, "Jigsaw piece", pieceKey, blockingErrors); + if (piece == null) { + continue; + } + String objectKey = piece.optString("object", "").trim(); + if (objectKey.isEmpty()) { + blockingErrors.add("Jigsaw piece '" + pieceKey + "' does not declare an object."); + } else if (!objectKeys.contains(objectKey)) { + blockingErrors.add("Jigsaw piece '" + pieceKey + "' references missing object '" + + objectKey + "'."); + } + JSONArray connectors = piece.optJSONArray("connectors"); + if (connectors == null) { + continue; + } + for (int connectorIndex = 0; connectorIndex < connectors.length(); connectorIndex++) { + JSONObject connector = connectors.optJSONObject(connectorIndex); + if (connector == null) { + continue; + } + String poolKey = connector.optString("pool", "").trim(); + if (!poolKey.isEmpty() && !poolKeys.contains(poolKey)) { + blockingErrors.add("Jigsaw piece '" + pieceKey + "' connectors[" + connectorIndex + + "] references missing pool '" + poolKey + "'."); + } + } + } + } + + static JSONObject readGraphJson(File file, + String resourceType, + String resourceKey, + List blockingErrors) { + try { + return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IOException | RuntimeException e) { + String reason = e.getMessage(); + if (reason == null || reason.isBlank()) { + reason = e.getClass().getSimpleName(); + } + blockingErrors.add(resourceType + " '" + resourceKey + "' has invalid JSON: " + reason); + return null; + } + } + + private static boolean isLegacyStructureIndex(String structureKey, JSONObject structure) { + return "structure-index".equals(structureKey) + && structure.has("counts") + && structure.has("structureSets") + && structure.has("iris"); + } + + static String structureHostType(String folderName) { + return switch (folderName) { + case "dimensions" -> "Dimension"; + case "regions" -> "Region"; + case "biomes" -> "Biome"; + default -> "Resource"; + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java new file mode 100644 index 000000000..8e6c73a01 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationIo.java @@ -0,0 +1,127 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pack; + +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public final class PackValidationIo { + private PackValidationIo() { + } + + static JSONObject readJson(File file) { + try { + return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IOException | RuntimeException parseFailure) { + // Every folder scanned through readJson is also scanned by a readGraphJson pass + // (validateUnsupportedStructureTransforms / validateStructureStartPools), which records the + // parse failure as a blocking error. Reporting here too would duplicate it per scan. + return null; + } + } + + static boolean isScannableJsonPath(Path path) { + String name = path.getFileName().toString(); + if (!name.endsWith(".json")) { + return false; + } + String str = path.toString().replace(File.separatorChar, '/'); + if (str.contains("/" + PackValidator.TRASH_ROOT + "/")) { + return false; + } + if (str.contains("/" + PackValidator.DATAPACK_IMPORTS + "/")) { + return false; + } + if (str.contains("/" + PackValidator.EXTERNAL_DATAPACKS + "/")) { + return false; + } + if (str.contains("/" + PackValidator.INTERNAL_DATAPACKS + "/")) { + return false; + } + if (str.contains("/" + PackValidator.DATAPACKS_FOLDER + "/")) { + return false; + } + if (str.contains("/" + PackValidator.CACHE_FOLDER + "/")) { + return false; + } + if (str.contains("/" + PackValidator.OBJECTS_FOLDER + "/")) { + return false; + } + if (str.contains("/.iris/")) { + return false; + } + return true; + } + + static List listJsonRecursive(File root) { + List out = new ArrayList<>(); + try (Stream stream = Files.walk(root.toPath())) { + stream.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(".json")) + .forEach(p -> out.add(p.toFile())); + } catch (Throwable ignored) { + } + return out; + } + + static String deriveKey(File resourceFolder, File resourceFile) { + Path relative = resourceFolder.toPath().relativize(resourceFile.toPath()); + String str = relative.toString().replace(File.separatorChar, '/'); + if (!str.endsWith(".json")) { + return null; + } + return str.substring(0, str.length() - ".json".length()); + } + + static String stripExtension(String name) { + int dot = name.lastIndexOf('.'); + return dot <= 0 ? name : name.substring(0, dot); + } + + public static Set listReferencedKeysFromCorpus(String corpus) { + Set keys = new HashSet<>(); + if (corpus == null) { + return keys; + } + int i = 0; + while (i < corpus.length()) { + int start = corpus.indexOf('"', i); + if (start < 0) { + break; + } + int end = corpus.indexOf('"', start + 1); + if (end < 0) { + break; + } + keys.add(corpus.substring(start + 1, end)); + i = end + 1; + } + return keys; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index 35b8ef83b..9bd56a3b5 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -18,53 +18,28 @@ package art.arcane.iris.core.pack; -import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType; -import art.arcane.iris.engine.object.IrisLoot; -import art.arcane.iris.engine.object.IrisLootReference; -import art.arcane.iris.engine.object.IrisLootTable; -import art.arcane.iris.engine.object.ObjectPlaceMode; -import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformEntityType; -import art.arcane.iris.spi.PlatformRegistries; -import art.arcane.volmlib.util.json.JSONArray; -import art.arcane.volmlib.util.json.JSONObject; - import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; import java.util.regex.Pattern; -import java.util.stream.Stream; public final class PackValidator { - private static final String TRASH_ROOT = ".iris-trash"; - private static final String DATAPACK_IMPORTS = "datapack-imports"; - private static final String EXTERNAL_DATAPACKS = "externaldatapacks"; - private static final String INTERNAL_DATAPACKS = "internaldatapacks"; - private static final String DATAPACKS_FOLDER = "datapacks"; - private static final String CACHE_FOLDER = "cache"; - private static final String OBJECTS_FOLDER = "objects"; - private static final String LOOT_FOLDER = "loot"; - private static final String DIMENSIONS_FOLDER = "dimensions"; - private static final String STRUCTURES_FOLDER = "structures"; - private static final String JIGSAW_POOLS_FOLDER = "jigsaw-pools"; - private static final String JIGSAW_PIECES_FOLDER = "jigsaw-pieces"; - private static final List STRUCTURE_HOST_FOLDERS = List.of(DIMENSIONS_FOLDER, "regions", "biomes"); - private static final List REMOVED_WORLDGEN_FIELDS = List.of("fluidBodies"); - private static final List UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale"); - private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); + static final String TRASH_ROOT = ".iris-trash"; + static final String DATAPACK_IMPORTS = "datapack-imports"; + static final String EXTERNAL_DATAPACKS = "externaldatapacks"; + static final String INTERNAL_DATAPACKS = "internaldatapacks"; + static final String DATAPACKS_FOLDER = "datapacks"; + static final String CACHE_FOLDER = "cache"; + static final String OBJECTS_FOLDER = "objects"; + static final String LOOT_FOLDER = "loot"; + static final String DIMENSIONS_FOLDER = "dimensions"; + static final String STRUCTURES_FOLDER = "structures"; + static final String JIGSAW_POOLS_FOLDER = "jigsaw-pools"; + static final String JIGSAW_PIECES_FOLDER = "jigsaw-pieces"; + static final List STRUCTURE_HOST_FOLDERS = List.of(DIMENSIONS_FOLDER, "regions", "biomes"); + static final List REMOVED_WORLDGEN_FIELDS = List.of("fluidBodies"); + static final List UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale"); + static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); private PackValidator() { } @@ -92,27 +67,27 @@ public final class PackValidator { return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); } - validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings); - blockingErrors.addAll(validateLootGraph(packFolder)); - blockingErrors.addAll(validateRemovedWorldgenFields(packFolder)); - blockingErrors.addAll(validateObjectSurfaceSupport(packFolder)); - blockingErrors.addAll(validateUnsupportedStructureTransforms(packFolder)); - blockingErrors.addAll(validateStructureGraph(packFolder)); + PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings); + blockingErrors.addAll(PackLootValidator.validateLootGraph(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateRemovedWorldgenFields(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateObjectSurfaceSupport(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateUnsupportedStructureTransforms(packFolder)); + blockingErrors.addAll(PackObjectSurfaceValidator.validateStructureGraph(packFolder)); StructureGraphPackValidator.Validation compiledStructures = StructureGraphPackValidator.validate( - packFolder.toPath(), collectPlacedStructureKeys(packFolder)); + packFolder.toPath(), PackObjectSurfaceValidator.collectPlacedStructureKeys(packFolder)); addDistinct(blockingErrors, compiledStructures.errors()); addDistinct(warnings, compiledStructures.warnings()); - blockingErrors.addAll(validateNativeStructureReplacements( + blockingErrors.addAll(PackNativeStructureValidator.validateNativeStructureReplacements( packFolder, compiledStructures.replacementOutputStructures(), compiledStructures.sampledVerticalEnvelopes())); - blockingErrors.addAll(validateSpawnerEntityReferences( + blockingErrors.addAll(PackSpawnValidator.validateSpawnerEntityReferences( new File(packFolder, "spawners"), new File(packFolder, "entities"))); - blockingErrors.addAll(validateCustomBiomeSpawns( - new File(packFolder, "biomes"), PackValidator::resolveEntitySpawnCategory)); + blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns( + new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory)); - runContentKeyValidation(packFolder, warnings); + ContentKeyValidator.runContentKeyValidation(packFolder, warnings); return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); } @@ -124,1816 +99,4 @@ public final class PackValidator { } } } - - static Set collectPlacedStructureKeys(File packFolder) { - Set structureKeys = new LinkedHashSet<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return structureKeys; - } - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - for (File resourceFile : listJsonRecursive(resourceFolder)) { - JSONObject resource = readJson(resourceFile); - if (resource == null) { - continue; - } - JSONArray placements = resource.optJSONArray("structures"); - if (placements == null) { - continue; - } - for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { - JSONObject placement = placements.optJSONObject(placementIndex); - if (placement == null) { - continue; - } - JSONArray references = placement.optJSONArray("structures"); - if (references == null) { - continue; - } - for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { - Object rawReference = references.opt(referenceIndex); - if (rawReference instanceof String structureKey && !structureKey.isBlank()) { - structureKeys.add(structureKey); - } - } - } - } - } - return Set.copyOf(structureKeys); - } - - static List validateUnsupportedStructureTransforms(File packFolder) { - List blockingErrors = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return blockingErrors; - } - - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - List resourceFiles = listJsonRecursive(resourceFolder); - resourceFiles.sort(Comparator.comparing(File::getPath)); - String resourceType = structureHostType(folderName); - for (File resourceFile : resourceFiles) { - JSONObject resource; - try { - resource = new JSONObject(Files.readString(resourceFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable ignored) { - continue; - } - - JSONArray placements = resource.optJSONArray("structures"); - if (placements == null) { - continue; - } - String resourceKey = deriveKey(resourceFolder, resourceFile); - for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { - JSONObject placement = placements.optJSONObject(placementIndex); - if (placement == null) { - continue; - } - for (String field : UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS) { - if (placement.has(field)) { - blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + placementIndex - + "] declares unsupported field '" + field - + "'. Structure placement transforms are not supported; remove the field."); - } - } - } - } - } - return blockingErrors; - } - - static List validateStructureGraph(File packFolder) { - List blockingErrors = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return blockingErrors; - } - - File structuresFolder = new File(packFolder, STRUCTURES_FOLDER); - File poolsFolder = new File(packFolder, JIGSAW_POOLS_FOLDER); - File piecesFolder = new File(packFolder, JIGSAW_PIECES_FOLDER); - File objectsFolder = new File(packFolder, OBJECTS_FOLDER); - Set structureKeys = deriveRegistrantKeysExact(structuresFolder); - Set poolKeys = deriveRegistrantKeysExact(poolsFolder); - Set pieceKeys = deriveRegistrantKeysExact(piecesFolder); - Set objectKeys = deriveObjectKeysExact(objectsFolder); - - validateStructurePlacements(packFolder, structureKeys, blockingErrors); - validateStructureStartPools(structuresFolder, poolKeys, blockingErrors); - validateJigsawPools(poolsFolder, poolKeys, pieceKeys, blockingErrors); - validateJigsawPieces(piecesFolder, poolKeys, objectKeys, blockingErrors); - return blockingErrors; - } - - static List validateLootGraph(File packFolder) { - List blockingErrors = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return blockingErrors; - } - - File lootFolder = new File(packFolder, LOOT_FOLDER); - Set lootKeys = deriveRegistrantKeysExact(lootFolder); - if (lootFolder.isDirectory()) { - List lootFiles = listJsonRecursive(lootFolder); - lootFiles.sort(Comparator.comparing(File::getPath)); - for (File lootFile : lootFiles) { - String lootKey = deriveKey(lootFolder, lootFile); - JSONObject table = readGraphJson(lootFile, "Loot table", lootKey, blockingErrors); - if (table != null) { - validateLootTable(lootKey, table, blockingErrors); - } - } - } - - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - List resourceFiles = listJsonRecursive(resourceFolder); - resourceFiles.sort(Comparator.comparing(File::getPath)); - for (File resourceFile : resourceFiles) { - JSONObject resource = readJson(resourceFile); - if (resource == null || !resource.has("loot")) { - continue; - } - String resourceType = structureHostType(folderName); - String resourceKey = deriveKey(resourceFolder, resourceFile); - validateLootReference(resourceType, resourceKey, resource.opt("loot"), lootKeys, blockingErrors); - } - } - return blockingErrors; - } - - static List validateRemovedWorldgenFields(File packFolder) { - List blockingErrors = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return blockingErrors; - } - - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - List resourceFiles = listJsonRecursive(resourceFolder); - resourceFiles.sort(Comparator.comparing(File::getPath)); - String resourceType = structureHostType(folderName); - for (File resourceFile : resourceFiles) { - JSONObject resource = readJson(resourceFile); - if (resource == null) { - continue; - } - String resourceKey = deriveKey(resourceFolder, resourceFile); - for (String field : REMOVED_WORLDGEN_FIELDS) { - if (resource.has(field)) { - blockingErrors.add(resourceType + " '" + resourceKey + "' declares removed field '" - + field + "'. Remove it because fluid-body generation is not supported."); - } - } - } - } - return blockingErrors; - } - - static List validateObjectSurfaceSupport(File packFolder) { - List blockingErrors = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return blockingErrors; - } - - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - List resourceFiles = listJsonRecursive(resourceFolder); - resourceFiles.sort(Comparator.comparing(File::getPath)); - String resourceType = structureHostType(folderName); - for (File resourceFile : resourceFiles) { - JSONObject resource = readJson(resourceFile); - if (resource == null) { - continue; - } - String path = resourceType + " '" + deriveKey(resourceFolder, resourceFile) + "'"; - if ("dimensions".equals(folderName)) { - validateOptionalIntegerRange(path, resource, "objectSurfaceSupportBuffer", 0, 16, blockingErrors); - validateOptionalBoolean(path, resource, "requireObjectSurfaceSupport", blockingErrors); - } - validateObjectPlacementSurfaceSupport(path, resource.optJSONArray("objects"), blockingErrors); - } - } - return blockingErrors; - } - - private static void validateObjectPlacementSurfaceSupport(String path, JSONArray placements, - List blockingErrors) { - if (placements == null) { - return; - } - for (int i = 0; i < placements.length(); i++) { - JSONObject placement = placements.optJSONObject(i); - if (placement == null) { - continue; - } - String placementPath = path + ".objects[" + i + "]"; - if (placement.has("surfaceOpeningClearance")) { - blockingErrors.add(placementPath + " declares removed field 'surfaceOpeningClearance'. " - + "Use surfaceSupportBuffer instead."); - } - validateOptionalIntegerRange(placementPath, placement, "surfaceSupportBuffer", 0, 16, blockingErrors); - validateOptionalIntegerRange(placementPath, placement, "surfaceSupportDepth", 1, 16, blockingErrors); - validateOptionalBoolean(placementPath, placement, "requireSurfaceSupport", blockingErrors); - } - } - - private static void validateOptionalBoolean(String path, JSONObject object, String field, - List blockingErrors) { - if (!object.has(field) || object.opt(field) == JSONObject.NULL) { - return; - } - if (!(object.opt(field) instanceof Boolean)) { - blockingErrors.add(path + "." + field + " must be a boolean."); - } - } - - private static void validateLootTable(String lootKey, JSONObject table, List blockingErrors) { - String path = "Loot table '" + lootKey + "'"; - Integer rarity = lootInteger(table, "rarity", 1, path, blockingErrors); - Integer minimumPicked = lootInteger(table, "minPicked", 1, path, blockingErrors); - Integer maximumPicked = lootInteger(table, "maxPicked", 5, path, blockingErrors); - Integer maximumTries = lootInteger(table, "maxTries", 10, path, blockingErrors); - requireMinimum(path + ".rarity", rarity, 1, blockingErrors); - requireMinimum(path + ".minPicked", minimumPicked, 0, blockingErrors); - requireMinimum(path + ".maxPicked", maximumPicked, 1, blockingErrors); - requireMinimum(path + ".maxTries", maximumTries, 1, blockingErrors); - requireMaximum(path + ".minPicked", minimumPicked, IrisLootTable.MAX_PICKED, blockingErrors); - requireMaximum(path + ".maxPicked", maximumPicked, IrisLootTable.MAX_PICKED, blockingErrors); - requireMaximum(path + ".maxTries", maximumTries, IrisLootTable.MAX_TRIES, blockingErrors); - requireOrdered(path + ".minPicked", minimumPicked, path + ".maxPicked", maximumPicked, blockingErrors); - - JSONArray entries = table.optJSONArray("loot"); - if (entries == null || entries.length() == 0) { - blockingErrors.add(path + ".loot must be a non-empty array."); - return; - } - for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) { - JSONObject entry = entries.optJSONObject(entryIndex); - String entryPath = path + ".loot[" + entryIndex + "]"; - if (entry == null) { - blockingErrors.add(entryPath + " must be an object."); - continue; - } - String type = entry.optString("type", "").trim(); - if (type.isEmpty()) { - blockingErrors.add(entryPath + ".type must not be blank."); - } - Integer entryRarity = lootInteger(entry, "rarity", 1, entryPath, blockingErrors); - Integer minimumAmount = lootInteger(entry, "minAmount", 1, entryPath, blockingErrors); - Integer maximumAmount = lootInteger(entry, "maxAmount", 1, entryPath, blockingErrors); - requireMinimum(entryPath + ".rarity", entryRarity, 1, blockingErrors); - requireMinimum(entryPath + ".minAmount", minimumAmount, 1, blockingErrors); - requireMinimum(entryPath + ".maxAmount", maximumAmount, 1, blockingErrors); - requireMaximum(entryPath + ".minAmount", minimumAmount, IrisLoot.MAX_AMOUNT, blockingErrors); - requireMaximum(entryPath + ".maxAmount", maximumAmount, IrisLoot.MAX_AMOUNT, blockingErrors); - requireOrdered(entryPath + ".minAmount", minimumAmount, - entryPath + ".maxAmount", maximumAmount, blockingErrors); - validateLootEnchantments(entryPath, entry.opt("enchantments"), blockingErrors); - } - } - - private static void validateLootEnchantments(String entryPath, Object rawEnchantments, - List blockingErrors) { - if (rawEnchantments == null || rawEnchantments == JSONObject.NULL) { - return; - } - if (!(rawEnchantments instanceof JSONArray enchantments)) { - blockingErrors.add(entryPath + ".enchantments must be an array."); - return; - } - for (int enchantmentIndex = 0; enchantmentIndex < enchantments.length(); enchantmentIndex++) { - JSONObject enchantment = enchantments.optJSONObject(enchantmentIndex); - String enchantmentPath = entryPath + ".enchantments[" + enchantmentIndex + "]"; - if (enchantment == null) { - blockingErrors.add(enchantmentPath + " must be an object."); - continue; - } - if (enchantment.optString("enchantment", "").isBlank()) { - blockingErrors.add(enchantmentPath + ".enchantment must not be blank."); - } - Integer minimumLevel = lootInteger(enchantment, "minLevel", 1, enchantmentPath, blockingErrors); - Integer maximumLevel = lootInteger(enchantment, "maxLevel", 1, enchantmentPath, blockingErrors); - requireMinimum(enchantmentPath + ".minLevel", minimumLevel, 1, blockingErrors); - requireMinimum(enchantmentPath + ".maxLevel", maximumLevel, 1, blockingErrors); - requireOrdered(enchantmentPath + ".minLevel", minimumLevel, - enchantmentPath + ".maxLevel", maximumLevel, blockingErrors); - if (enchantment.has("chance")) { - Object rawChance = enchantment.opt("chance"); - if (!(rawChance instanceof Number number) - || !Double.isFinite(number.doubleValue()) - || number.doubleValue() < 0D - || number.doubleValue() > 1D) { - blockingErrors.add(enchantmentPath + ".chance must be a finite number from 0 to 1."); - } - } - } - } - - private static void validateLootReference(String resourceType, String resourceKey, Object rawLoot, - Set lootKeys, List blockingErrors) { - String path = resourceType + " '" + resourceKey + "'.loot"; - if (!(rawLoot instanceof JSONObject reference)) { - blockingErrors.add(path + " must be an object."); - return; - } - if (reference.has("mode")) { - Object rawMode = reference.opt("mode"); - if (!(rawMode instanceof String mode) - || !Set.of("ADD", "CLEAR", "REPLACE", "FALLBACK").contains(mode)) { - blockingErrors.add(path + ".mode must be ADD, CLEAR, REPLACE, or FALLBACK."); - } - } - if (reference.has("multiplier")) { - Object rawMultiplier = reference.opt("multiplier"); - if (!(rawMultiplier instanceof Number multiplier) - || !Double.isFinite(multiplier.doubleValue()) - || multiplier.doubleValue() < 0D - || multiplier.doubleValue() > IrisLootReference.MAX_MULTIPLIER) { - blockingErrors.add(path + ".multiplier must be a finite number from 0 to " - + (int) IrisLootReference.MAX_MULTIPLIER + "."); - } - } - if (!reference.has("tables")) { - return; - } - JSONArray tables = reference.optJSONArray("tables"); - if (tables == null) { - blockingErrors.add(path + ".tables must be an array."); - return; - } - for (int tableIndex = 0; tableIndex < tables.length(); tableIndex++) { - Object rawTableKey = tables.opt(tableIndex); - if (!(rawTableKey instanceof String tableKey) || tableKey.isBlank()) { - blockingErrors.add(path + ".tables[" + tableIndex + "] must name a loot table."); - } else if (!lootKeys.contains(tableKey)) { - blockingErrors.add(path + ".tables[" + tableIndex - + "] references missing loot table '" + tableKey + "'."); - } - } - } - - private static Integer lootInteger(JSONObject object, String field, int defaultValue, - String path, List blockingErrors) { - if (!object.has(field)) { - return defaultValue; - } - Object rawValue = object.opt(field); - if (!(rawValue instanceof Number number) - || !Double.isFinite(number.doubleValue()) - || number.doubleValue() != Math.rint(number.doubleValue()) - || number.longValue() < Integer.MIN_VALUE - || number.longValue() > Integer.MAX_VALUE) { - blockingErrors.add(path + "." + field + " must be an integer."); - return null; - } - return number.intValue(); - } - - private static void requireMinimum(String fieldPath, Integer value, int minimum, - List blockingErrors) { - if (value != null && value < minimum) { - blockingErrors.add(fieldPath + " must be at least " + minimum + "."); - } - } - - private static void requireMaximum(String fieldPath, Integer value, int maximum, - List blockingErrors) { - if (value != null && value > maximum) { - blockingErrors.add(fieldPath + " must be at most " + maximum + "."); - } - } - - private static void requireOrdered(String minimumPath, Integer minimum, String maximumPath, - Integer maximum, List blockingErrors) { - if (minimum != null && maximum != null && minimum > maximum) { - blockingErrors.add(minimumPath + " must not exceed " + maximumPath + "."); - } - } - - static List validateNativeStructureReplacements( - File packFolder, - Set replacementOutputStructures, - Map> sampledVerticalEnvelopes - ) { - List blockingErrors = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory()) { - return blockingErrors; - } - Set viableStructures = replacementOutputStructures == null - ? Set.of() : replacementOutputStructures; - Map> verticalEnvelopes = - sampledVerticalEnvelopes == null ? Map.of() : sampledVerticalEnvelopes; - File structuresFolder = new File(packFolder, STRUCTURES_FOLDER); - Map structures = new HashMap<>(); - for (File structureFile : listJsonRecursive(structuresFolder)) { - JSONObject structure = readJson(structureFile); - if (structure != null) { - structures.put(deriveKey(structuresFolder, structureFile), structure); - } - } - - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - List resourceFiles = listJsonRecursive(resourceFolder); - resourceFiles.sort(Comparator.comparing(File::getPath)); - String resourceType = structureHostType(folderName); - for (File resourceFile : resourceFiles) { - JSONObject resource = readJson(resourceFile); - if (resource == null) { - continue; - } - JSONArray placements = resource.optJSONArray("structures"); - if (placements == null) { - continue; - } - String resourceKey = deriveKey(resourceFolder, resourceFile); - for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { - JSONObject placement = placements.optJSONObject(placementIndex); - if (placement == null || !placement.has("nativeSuppression")) { - continue; - } - Object rawSuppression = placement.opt("nativeSuppression"); - if (!(rawSuppression instanceof String suppression)) { - blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" - + placementIndex + "].nativeSuppression must be NONE or REPLACE_SOURCE."); - continue; - } - if ("NONE".equals(suppression)) { - continue; - } - if (!"REPLACE_SOURCE".equals(suppression)) { - blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" - + placementIndex + "].nativeSuppression has unsupported value '" - + suppression + "'. Use NONE or REPLACE_SOURCE."); - continue; - } - if (!DIMENSIONS_FOLDER.equals(folderName)) { - blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" - + placementIndex + "] requests REPLACE_SOURCE, but native replacement is only" - + " valid on dimension-level placements."); - continue; - } - JSONArray references = placement.optJSONArray("structures"); - JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); - if (nativeStructures != null && nativeStructures.length() > 0) { - continue; - } - if (references == null || references.length() == 0) { - blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex - + "] requests REPLACE_SOURCE without any structure backend."); - continue; - } - for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { - Object rawReference = references.opt(referenceIndex); - if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) { - blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex - + "].structures[" + referenceIndex - + "] must name an Iris structure for REPLACE_SOURCE."); - continue; - } - JSONObject structure = structures.get(structureKey); - if (structure == null) { - blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex - + "] cannot REPLACE_SOURCE with missing or invalid structure '" - + structureKey + "'."); - continue; - } - String vanillaSource = structure.optString("vanillaSource", "").trim(); - if (!RESOURCE_KEY_PATTERN.matcher(vanillaSource).matches()) { - blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex - + "] requests REPLACE_SOURCE for structure '" + structureKey - + "', but its vanillaSource is not a valid namespaced registry key."); - } - if (!viableStructures.contains(structureKey)) { - blockingErrors.add("Dimension '" + resourceKey + "' structures[" + placementIndex - + "] requests REPLACE_SOURCE for structure '" + structureKey - + "', but that structure is not runtime-viable. Native generation will not" - + " be used as a fallback."); - continue; - } - validateReplacementVerticalEnvelope( - resourceKey, - resource, - placementIndex, - placement, - structureKey, - structure, - verticalEnvelopes.get(structureKey), - blockingErrors); - } - } - } - } - return blockingErrors; - } - - private static void validateReplacementVerticalEnvelope( - String dimensionKey, - JSONObject dimension, - int placementIndex, - JSONObject placement, - String structureKey, - JSONObject structure, - List sampledVerticalEnvelopes, - List blockingErrors - ) { - String context = "Dimension '" + dimensionKey + "' structures[" + placementIndex - + "] REPLACE_SOURCE structure '" + structureKey + "'"; - if (sampledVerticalEnvelopes == null || sampledVerticalEnvelopes.isEmpty()) { - blockingErrors.add(context + " has no sampled vertical envelope. Native generation will not" - + " be used as a fallback."); - return; - } - - DimensionVerticalBounds worldBounds = resolveDimensionVerticalBounds(dimension, context, blockingErrors); - PlacementVerticalBounds placementBounds = resolvePlacementVerticalBounds(placement, context, blockingErrors); - ObjectPlaceMode placeMode = resolvePlaceMode(structure, context, blockingErrors); - if (worldBounds == null || placementBounds == null || placeMode == null) { - return; - } - - for (StructureGraphPackValidator.SampledVerticalEnvelope sampled : sampledVerticalEnvelopes) { - boolean exactY = placementBounds.underground() - || sampled.pieceCount() > 1 - || placeMode == ObjectPlaceMode.STRUCTURE_PIECE - || placeMode == ObjectPlaceMode.FLOATING; - if (!exactY) { - continue; - } - - long minimumYOffset = sampled.minimumYOffset(); - long maximumYOffset = sampled.maximumYOffset(); - boolean surfaceAligned = !placementBounds.underground() - && sampled.pieceCount() > 1 - && placeMode != ObjectPlaceMode.STRUCTURE_PIECE - && placeMode != ObjectPlaceMode.FLOATING; - if (surfaceAligned) { - maximumYOffset -= minimumYOffset; - minimumYOffset = 0L; - } - - boolean fitsConfiguredRange; - if (placementBounds.underground()) { - long minimumAnchor = Math.max( - Math.max(placementBounds.minimumY(), worldBounds.minimumY()), - worldBounds.minimumY() - minimumYOffset); - long maximumAnchor = Math.min( - Math.min(placementBounds.maximumY(), worldBounds.maximumY()), - worldBounds.maximumY() - maximumYOffset); - fitsConfiguredRange = minimumAnchor <= maximumAnchor; - } else { - long minimumTerrainY = Math.max(placementBounds.minimumY(), worldBounds.minimumY()); - long maximumTerrainY = Math.min(placementBounds.maximumY(), worldBounds.maximumY()); - fitsConfiguredRange = minimumTerrainY <= maximumTerrainY - && minimumTerrainY + minimumYOffset >= worldBounds.minimumY() - && maximumTerrainY + maximumYOffset <= worldBounds.maximumY(); - } - if (fitsConfiguredRange) { - continue; - } - - String alignment = surfaceAligned ? "surface-aligned " : ""; - blockingErrors.add(context + " sampled seed " + sampled.seed() + " has an " + alignment - + "exact-Y piece envelope " + minimumYOffset + ".." + maximumYOffset - + " relative to its anchor, which cannot fit placement band " - + placementBounds.minimumY() + ".." + placementBounds.maximumY() - + " inside writable world " + worldBounds.minimumY() + ".." + worldBounds.maximumY() - + ". Native generation will not be used as a fallback."); - return; - } - } - - private static DimensionVerticalBounds resolveDimensionVerticalBounds( - JSONObject dimension, - String context, - List blockingErrors - ) { - long dimensionMinimum = -64L; - long dimensionMaximum = 320L; - if (dimension.has("dimensionHeight")) { - JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight"); - if (dimensionHeight == null) { - blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight" - + " must be an object."); - return null; - } - Long configuredMinimum = integralJsonNumber(dimensionHeight, "min", 16L); - Long configuredMaximum = integralJsonNumber(dimensionHeight, "max", 32L); - if (configuredMinimum == null || configuredMaximum == null) { - blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight" - + " min and max must be finite integer values."); - return null; - } - dimensionMinimum = configuredMinimum; - dimensionMaximum = configuredMaximum; - } - - long writableMinimum = dimensionMinimum + 1L; - long writableMaximum = dimensionMaximum - 1L; - if (writableMinimum > writableMaximum) { - blockingErrors.add(context + " cannot validate its vertical envelope because dimensionHeight " - + dimensionMinimum + ".." + dimensionMaximum + " has no writable structure range."); - return null; - } - return new DimensionVerticalBounds(writableMinimum, writableMaximum); - } - - private static PlacementVerticalBounds resolvePlacementVerticalBounds( - JSONObject placement, - String context, - List blockingErrors - ) { - boolean underground = false; - if (placement.has("underground")) { - Object rawUnderground = placement.opt("underground"); - if (!(rawUnderground instanceof Boolean configuredUnderground)) { - blockingErrors.add(context + " cannot validate its vertical envelope because underground" - + " must be true or false."); - return null; - } - underground = configuredUnderground; - } - Long configuredMinimum = integralJsonNumber(placement, "minHeight", -2032L); - Long configuredMaximum = integralJsonNumber(placement, "maxHeight", 2032L); - if (configuredMinimum == null || configuredMaximum == null) { - blockingErrors.add(context + " cannot validate its vertical envelope because minHeight and" - + " maxHeight must be finite integer values."); - return null; - } - long minimumY = underground - ? Math.min(configuredMinimum, configuredMaximum) : configuredMinimum; - long maximumY = underground - ? Math.max(configuredMinimum, configuredMaximum) : configuredMaximum; - return new PlacementVerticalBounds(minimumY, maximumY, underground); - } - - private static ObjectPlaceMode resolvePlaceMode( - JSONObject structure, - String context, - List blockingErrors - ) { - if (!structure.has("placeMode")) { - return ObjectPlaceMode.STRUCTURE_PIECE; - } - Object rawPlaceMode = structure.opt("placeMode"); - if (!(rawPlaceMode instanceof String placeModeName)) { - blockingErrors.add(context + " cannot validate its vertical envelope because placeMode must name" - + " an Iris object place mode."); - return null; - } - try { - return ObjectPlaceMode.valueOf(placeModeName); - } catch (IllegalArgumentException exception) { - blockingErrors.add(context + " cannot validate its vertical envelope because placeMode '" - + placeModeName + "' is not supported."); - return null; - } - } - - private static Long integralJsonNumber(JSONObject owner, String field, long defaultValue) { - if (!owner.has(field)) { - return defaultValue; - } - Object rawValue = owner.opt(field); - if (!(rawValue instanceof Number number)) { - return null; - } - double value = number.doubleValue(); - if (!Double.isFinite(value) || value != Math.rint(value) - || value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { - return null; - } - return (long) value; - } - - private static void validateStructurePlacements(File packFolder, - Set structureKeys, - List blockingErrors) { - Set registeredStructures = registeredStructureKeys(); - Set registeredJigsaws = registeredJigsawKeys(); - Set registeredPools = registeredTemplatePoolKeys(); - for (String folderName : STRUCTURE_HOST_FOLDERS) { - File resourceFolder = new File(packFolder, folderName); - if (!resourceFolder.isDirectory()) { - continue; - } - List resourceFiles = listJsonRecursive(resourceFolder); - resourceFiles.sort(Comparator.comparing(File::getPath)); - String resourceType = structureHostType(folderName); - for (File resourceFile : resourceFiles) { - JSONObject resource = readJson(resourceFile); - if (resource == null) { - continue; - } - JSONArray placements = resource.optJSONArray("structures"); - if (placements == null) { - continue; - } - String resourceKey = deriveKey(resourceFolder, resourceFile); - for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) { - JSONObject placement = placements.optJSONObject(placementIndex); - if (placement == null) { - continue; - } - JSONArray references = placement.optJSONArray("structures"); - JSONArray nativeStructures = placement.optJSONArray("nativeStructures"); - boolean hasIrisStructures = references != null && references.length() > 0; - boolean hasNativeStructures = nativeStructures != null && nativeStructures.length() > 0; - String placementPath = resourceType + " '" + resourceKey + "' structures[" - + placementIndex + "]"; - if (hasIrisStructures == hasNativeStructures) { - blockingErrors.add(placementPath - + " must declare exactly one non-empty backend: structures or nativeStructures."); - continue; - } - if (hasNativeStructures) { - validateNativeStructures( - placementPath, placement, nativeStructures, - registeredStructures, registeredJigsaws, - registeredPools, blockingErrors); - continue; - } - for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) { - Object rawReference = references.opt(referenceIndex); - if (!(rawReference instanceof String structureKey) || structureKey.isBlank()) { - continue; - } - if (!structureKeys.contains(structureKey)) { - blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" - + placementIndex + "].structures[" + referenceIndex - + "] references missing structure '" + structureKey + "'."); - } - } - } - } - } - } - - private static Set registeredJigsawKeys() { - try { - List registered = IrisPlatforms.get().structureHooks().jigsawStructureKeys(); - if (registered == null || registered.isEmpty()) { - return Set.of(); - } - Set keys = new HashSet<>(); - for (String key : registered) { - if (key != null && !key.isBlank()) { - keys.add(key.toLowerCase(Locale.ROOT)); - } - } - return Set.copyOf(keys); - } catch (Throwable ignored) { - return Set.of(); - } - } - - private static Set registeredStructureKeys() { - try { - List registered = IrisPlatforms.get().structureHooks().structureKeys(); - if (registered == null || registered.isEmpty()) { - return Set.of(); - } - Set keys = new HashSet<>(); - for (String key : registered) { - if (key != null && !key.isBlank()) { - keys.add(key.toLowerCase(Locale.ROOT)); - } - } - return Set.copyOf(keys); - } catch (Throwable ignored) { - return Set.of(); - } - } - - private static Set registeredTemplatePoolKeys() { - try { - List registered = IrisPlatforms.get().structureHooks().templatePoolKeys(); - if (registered == null || registered.isEmpty()) { - return Set.of(); - } - Set keys = new HashSet<>(); - for (String key : registered) { - if (key != null && !key.isBlank()) { - keys.add(key.toLowerCase(Locale.ROOT)); - } - } - return Set.copyOf(keys); - } catch (Throwable ignored) { - return Set.of(); - } - } - - private static void validateNativeStructures(String placementPath, JSONObject placement, - JSONArray nativeStructures, - Set registeredStructures, - Set registeredJigsaws, - Set registeredPools, - List blockingErrors) { - for (int sourceIndex = 0; sourceIndex < nativeStructures.length(); sourceIndex++) { - String sourcePath = placementPath + ".nativeStructures[" + sourceIndex + "]"; - JSONObject source = nativeStructures.optJSONObject(sourceIndex); - if (source == null) { - blockingErrors.add(sourcePath + " must be an object."); - continue; - } - String structureKey = source.optString("structure", "").trim(); - if (!RESOURCE_KEY_PATTERN.matcher(structureKey).matches()) { - blockingErrors.add(sourcePath + ".structure must be a namespaced registry key."); - } else if (!registeredStructures.isEmpty() - && !registeredStructures.contains(structureKey.toLowerCase(Locale.ROOT))) { - blockingErrors.add(sourcePath + ".structure '" + structureKey - + "' is not a registered structure."); - } - Integer weight = lootInteger(source, "weight", 1, sourcePath, blockingErrors); - requireMinimum(sourcePath + ".weight", weight, 1, blockingErrors); - JSONObject jigsaw = source.optJSONObject("jigsaw"); - if (source.has("jigsaw") && source.opt("jigsaw") != JSONObject.NULL && jigsaw == null) { - blockingErrors.add(sourcePath + ".jigsaw must be an object."); - } else if (jigsaw != null) { - if (!registeredJigsaws.isEmpty() - && !registeredJigsaws.contains(structureKey.toLowerCase(Locale.ROOT))) { - blockingErrors.add(sourcePath - + ".jigsaw requires a registered jigsaw structure."); - } - validateJigsawAssembly( - sourcePath + ".jigsaw", jigsaw, registeredPools, blockingErrors); - } - } - validateNativeTerrain(placementPath, placement, blockingErrors); - } - - private static void validateJigsawAssembly(String path, JSONObject assembly, - Set registeredPools, - List blockingErrors) { - validateOptionalResourceKey(path, assembly, "startPool", false, blockingErrors); - String startPool = assembly.optString("startPool", "").trim(); - if (!startPool.isEmpty() && !registeredPools.isEmpty() - && !registeredPools.contains(startPool.toLowerCase(Locale.ROOT))) { - blockingErrors.add(path + ".startPool '" + startPool - + "' is not a registered template pool."); - } - validateOptionalResourceKey(path, assembly, "startJigsawName", true, blockingErrors); - validateOptionalIntegerRange(path, assembly, "maxDepth", 0, 20, blockingErrors); - validateOptionalIntegerRange(path, assembly, "maxDistanceHorizontal", 1, 128, blockingErrors); - validateOptionalIntegerRange(path, assembly, "maxDistanceVertical", 1, 4064, blockingErrors); - validateOptionalIntegerRange( - path, assembly, "dimensionPaddingBottom", 0, Integer.MAX_VALUE, blockingErrors); - validateOptionalIntegerRange( - path, assembly, "dimensionPaddingTop", 0, Integer.MAX_VALUE, blockingErrors); - if (assembly.has("useExpansionHack") - && !(assembly.opt("useExpansionHack") instanceof Boolean)) { - blockingErrors.add(path + ".useExpansionHack must be a boolean."); - } - validateOptionalEnum(path, assembly, "projectStartToHeightmap", - Set.of("SOURCE", "NONE", "WORLD_SURFACE_WG", "WORLD_SURFACE", - "OCEAN_FLOOR_WG", "OCEAN_FLOOR", "MOTION_BLOCKING", - "MOTION_BLOCKING_NO_LEAVES"), blockingErrors); - validateOptionalEnum(path, assembly, "liquidSettings", - Set.of("SOURCE", "IGNORE_WATERLOGGING", "APPLY_WATERLOGGING"), blockingErrors); - } - - private static void validateNativeTerrain(String path, JSONObject placement, - List blockingErrors) { - JSONObject terrain = placement.optJSONObject("terrain"); - if (placement.has("terrain") && placement.opt("terrain") != JSONObject.NULL && terrain == null) { - blockingErrors.add(path + ".terrain must be an object."); - return; - } - if (terrain == null) { - return; - } - validateOptionalEnum(path + ".terrain", terrain, "mode", - Set.of("SOURCE", "PRESERVE", "BORE", "FORCE_CARVE", "VACUUM", "ENCASE"), blockingErrors); - validateOptionalIntegerRange(path + ".terrain", terrain, - "horizontalPadding", 0, 128, blockingErrors); - validateOptionalIntegerRange(path + ".terrain", terrain, - "ceilingPadding", 0, 128, blockingErrors); - validateOptionalIntegerRange(path + ".terrain", terrain, - "floorPadding", 0, 64, blockingErrors); - validateOptionalDoubleRange(path + ".terrain", terrain, - "erosionStrength", 0D, 1D, blockingErrors); - validateOptionalDoubleRange(path + ".terrain", terrain, - "erosionFrequency", 0.001D, 1D, blockingErrors); - validateOptionalDoubleRange(path + ".terrain", terrain, - "lobeFrequency", 0D, 1D, blockingErrors); - validateOptionalDoubleRange(path + ".terrain", terrain, - "lobeStrength", 0D, 1D, blockingErrors); - if (terrain.has("encasePalette") && terrain.opt("encasePalette") != JSONObject.NULL - && terrain.optJSONObject("encasePalette") == null) { - blockingErrors.add(path + ".terrain.encasePalette must be an object."); - } - } - - private static void validateOptionalResourceKey(String path, JSONObject object, String field, - boolean allowNone, List blockingErrors) { - if (!object.has(field)) { - return; - } - Object rawValue = object.opt(field); - if (!(rawValue instanceof String value)) { - blockingErrors.add(path + "." + field + " must be a string."); - return; - } - String normalized = value.trim(); - if (normalized.isEmpty() || allowNone && "NONE".equalsIgnoreCase(normalized)) { - return; - } - if (!RESOURCE_KEY_PATTERN.matcher(normalized).matches()) { - blockingErrors.add(path + "." + field + " must be a namespaced registry key."); - } - } - - private static void validateOptionalIntegerRange(String path, JSONObject object, String field, - int minimum, int maximum, - List blockingErrors) { - if (!object.has(field) || object.opt(field) == JSONObject.NULL) { - return; - } - Integer value = lootInteger(object, field, minimum, path, blockingErrors); - requireMinimum(path + "." + field, value, minimum, blockingErrors); - requireMaximum(path + "." + field, value, maximum, blockingErrors); - } - - private static void validateOptionalDoubleRange(String path, JSONObject object, String field, - double minimum, double maximum, - List blockingErrors) { - if (!object.has(field) || object.opt(field) == JSONObject.NULL) { - return; - } - String fieldPath = path + "." + field; - Object rawValue = object.opt(field); - if (!(rawValue instanceof Number number) || !Double.isFinite(number.doubleValue())) { - blockingErrors.add(fieldPath + " must be a number."); - return; - } - double value = number.doubleValue(); - if (value < minimum) { - blockingErrors.add(fieldPath + " must be at least " + minimum + "."); - } - if (value > maximum) { - blockingErrors.add(fieldPath + " must be at most " + maximum + "."); - } - } - - private static void validateOptionalEnum(String path, JSONObject object, String field, - Set values, List blockingErrors) { - if (!object.has(field)) { - return; - } - Object rawValue = object.opt(field); - if (!(rawValue instanceof String value) || !values.contains(value)) { - blockingErrors.add(path + "." + field + " must be one of " + values + "."); - } - } - - private static void validateStructureStartPools(File structuresFolder, - Set poolKeys, - List blockingErrors) { - if (!structuresFolder.isDirectory()) { - return; - } - List structureFiles = listJsonRecursive(structuresFolder); - structureFiles.sort(Comparator.comparing(File::getPath)); - for (File structureFile : structureFiles) { - String structureKey = deriveKey(structuresFolder, structureFile); - JSONObject structure = readGraphJson(structureFile, "Structure", structureKey, blockingErrors); - if (structure == null || isLegacyStructureIndex(structureKey, structure)) { - continue; - } - String startPool = structure.optString("startPool", "").trim(); - if (startPool.isEmpty()) { - blockingErrors.add("Structure '" + structureKey + "' does not declare a startPool."); - } else if (!poolKeys.contains(startPool)) { - blockingErrors.add("Structure '" + structureKey + "' references missing start pool '" - + startPool + "'."); - } - } - } - - private static void validateJigsawPools(File poolsFolder, - Set poolKeys, - Set pieceKeys, - List blockingErrors) { - if (!poolsFolder.isDirectory()) { - return; - } - List poolFiles = listJsonRecursive(poolsFolder); - poolFiles.sort(Comparator.comparing(File::getPath)); - for (File poolFile : poolFiles) { - String poolKey = deriveKey(poolsFolder, poolFile); - JSONObject pool = readGraphJson(poolFile, "Jigsaw pool", poolKey, blockingErrors); - if (pool == null) { - continue; - } - JSONArray entries = pool.optJSONArray("pieces"); - if (entries != null) { - for (int entryIndex = 0; entryIndex < entries.length(); entryIndex++) { - JSONObject entry = entries.optJSONObject(entryIndex); - if (entry == null) { - continue; - } - String pieceKey = entry.optString("piece", "").trim(); - if (!pieceKey.isEmpty() && !pieceKeys.contains(pieceKey)) { - blockingErrors.add("Jigsaw pool '" + poolKey + "' pieces[" + entryIndex - + "] references missing piece '" + pieceKey + "'."); - } - } - } - String fallback = pool.optString("fallback", "").trim(); - if (!fallback.isEmpty() && !poolKeys.contains(fallback)) { - blockingErrors.add("Jigsaw pool '" + poolKey + "' references missing fallback pool '" - + fallback + "'."); - } - } - } - - private static void validateJigsawPieces(File piecesFolder, - Set poolKeys, - Set objectKeys, - List blockingErrors) { - if (!piecesFolder.isDirectory()) { - return; - } - List pieceFiles = listJsonRecursive(piecesFolder); - pieceFiles.sort(Comparator.comparing(File::getPath)); - for (File pieceFile : pieceFiles) { - String pieceKey = deriveKey(piecesFolder, pieceFile); - JSONObject piece = readGraphJson(pieceFile, "Jigsaw piece", pieceKey, blockingErrors); - if (piece == null) { - continue; - } - String objectKey = piece.optString("object", "").trim(); - if (objectKey.isEmpty()) { - blockingErrors.add("Jigsaw piece '" + pieceKey + "' does not declare an object."); - } else if (!objectKeys.contains(objectKey)) { - blockingErrors.add("Jigsaw piece '" + pieceKey + "' references missing object '" - + objectKey + "'."); - } - JSONArray connectors = piece.optJSONArray("connectors"); - if (connectors == null) { - continue; - } - for (int connectorIndex = 0; connectorIndex < connectors.length(); connectorIndex++) { - JSONObject connector = connectors.optJSONObject(connectorIndex); - if (connector == null) { - continue; - } - String poolKey = connector.optString("pool", "").trim(); - if (!poolKey.isEmpty() && !poolKeys.contains(poolKey)) { - blockingErrors.add("Jigsaw piece '" + pieceKey + "' connectors[" + connectorIndex - + "] references missing pool '" + poolKey + "'."); - } - } - } - } - - private static JSONObject readGraphJson(File file, - String resourceType, - String resourceKey, - List blockingErrors) { - try { - return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8)); - } catch (IOException | RuntimeException e) { - String reason = e.getMessage(); - if (reason == null || reason.isBlank()) { - reason = e.getClass().getSimpleName(); - } - blockingErrors.add(resourceType + " '" + resourceKey + "' has invalid JSON: " + reason); - return null; - } - } - - private static JSONObject readJson(File file) { - try { - return new JSONObject(Files.readString(file.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable ignored) { - return null; - } - } - - private static boolean isLegacyStructureIndex(String structureKey, JSONObject structure) { - return "structure-index".equals(structureKey) - && structure.has("counts") - && structure.has("structureSets") - && structure.has("iris"); - } - - private static String structureHostType(String folderName) { - return switch (folderName) { - case "dimensions" -> "Dimension"; - case "regions" -> "Region"; - case "biomes" -> "Biome"; - default -> "Resource"; - }; - } - - static List validateSpawnerEntityReferences(File spawnersFolder, File entitiesFolder) { - List blockingErrors = new ArrayList<>(); - if (spawnersFolder == null || !spawnersFolder.isDirectory()) { - return blockingErrors; - } - - Path entityRoot = entitiesFolder.toPath().toAbsolutePath().normalize(); - Set validEntityFiles = new HashSet<>(); - Map invalidEntityFiles = new HashMap<>(); - List spawnerFiles = listJsonRecursive(spawnersFolder); - spawnerFiles.sort(Comparator.comparing(File::getPath)); - for (File spawnerFile : spawnerFiles) { - String spawnerKey = deriveKey(spawnersFolder, spawnerFile); - JSONObject spawner; - try { - spawner = new JSONObject(Files.readString(spawnerFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable e) { - blockingErrors.add("Spawner '" + spawnerKey + "' has invalid JSON: " + e.getMessage()); - continue; - } - - validateSpawnerSpawnEntries(spawnerKey, spawner, "spawns", entityRoot, - validEntityFiles, invalidEntityFiles, blockingErrors); - validateSpawnerSpawnEntries(spawnerKey, spawner, "initialSpawns", entityRoot, - validEntityFiles, invalidEntityFiles, blockingErrors); - } - return blockingErrors; - } - - private static void validateSpawnerSpawnEntries(String spawnerKey, - JSONObject spawner, - String field, - Path entityRoot, - Set validEntityFiles, - Map invalidEntityFiles, - List blockingErrors) { - if (!spawner.has(field)) { - return; - } - JSONArray entries = spawner.optJSONArray(field); - if (entries == null) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " must be an array."); - return; - } - - for (int index = 0; index < entries.length(); index++) { - JSONObject entry = entries.optJSONObject(index); - if (entry == null) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field - + " has a non-object entry at index " + index + "."); - continue; - } - if (!entry.has("entity") || entry.isNull("entity")) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field - + " has an entry without an entity reference at index " + index + "."); - continue; - } - - Object rawEntity = entry.get("entity"); - if (!(rawEntity instanceof String entityKey)) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field - + " entity reference at index " + index + " must be a string."); - continue; - } - if (entityKey.isBlank()) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field - + " has a blank entity reference at index " + index + "."); - continue; - } - - Path entityFile; - try { - if (entityKey.indexOf('\\') >= 0) { - throw new IllegalArgumentException("backslash path separators are not portable"); - } - entityFile = entityRoot.resolve(entityKey + ".json").normalize(); - } catch (RuntimeException e) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index - + " has invalid entity reference '" + entityKey + "': " + e.getMessage()); - continue; - } - if (!entityFile.startsWith(entityRoot)) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index - + " has unsafe entity reference '" + entityKey + "'."); - continue; - } - if (!Files.isRegularFile(entityFile)) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index - + " references missing entity '" + entityKey + "'."); - continue; - } - - String invalidJson = invalidEntityFiles.get(entityFile); - if (invalidJson != null) { - blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index - + " references malformed entity '" + entityKey + "': " + invalidJson); - continue; - } - if (validEntityFiles.contains(entityFile)) { - continue; - } - - try { - new JSONObject(Files.readString(entityFile, StandardCharsets.UTF_8)); - validEntityFiles.add(entityFile); - } catch (Throwable e) { - String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); - invalidEntityFiles.put(entityFile, message); - blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index - + " references malformed entity '" + entityKey + "': " + message); - } - } - } - - static List validateCustomBiomeSpawns(File biomesFolder, Function categoryResolver) { - List blockingErrors = new ArrayList<>(); - if (biomesFolder == null || !biomesFolder.isDirectory()) { - return blockingErrors; - } - - List biomeFiles = listJsonRecursive(biomesFolder); - biomeFiles.sort(Comparator.comparing(File::getPath)); - for (File biomeFile : biomeFiles) { - String biomeKey = deriveKey(biomesFolder, biomeFile); - JSONObject biome; - try { - biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable e) { - blockingErrors.add("Biome '" + biomeKey + "' has invalid JSON: " + e.getMessage()); - continue; - } - - JSONArray derivatives = biome.optJSONArray("customDerivitives"); - if (derivatives == null) { - if (biome.has("customDerivitives") && !biome.isNull("customDerivitives")) { - blockingErrors.add("Biome '" + biomeKey + "' customDerivitives must be an array."); - } - continue; - } - for (int derivativeIndex = 0; derivativeIndex < derivatives.length(); derivativeIndex++) { - JSONObject derivative = derivatives.optJSONObject(derivativeIndex); - if (derivative == null) { - blockingErrors.add("Biome '" + biomeKey + "' has a non-object custom derivative at index " + derivativeIndex + "."); - continue; - } - validateCustomBiomeDerivativeTags(biomeKey, derivative, derivativeIndex, blockingErrors); - validateCustomBiomeDerivativeSpawns( - biomeKey, derivative, derivativeIndex, categoryResolver, blockingErrors); - } - } - return blockingErrors; - } - - private static void validateCustomBiomeDerivativeTags(String biomeKey, - JSONObject derivative, - int derivativeIndex, - List blockingErrors) { - if (!derivative.has("tags") || derivative.isNull("tags")) { - return; - } - String derivativeId = derivative.optString("id", "#" + derivativeIndex); - JSONArray tags = derivative.optJSONArray("tags"); - if (tags == null) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' tags must be an array."); - return; - } - for (int tagIndex = 0; tagIndex < tags.length(); tagIndex++) { - Object rawTag = tags.opt(tagIndex); - if (!(rawTag instanceof String tag)) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' has a non-string tag at index " + tagIndex + "."); - continue; - } - String normalized = tag.trim().toLowerCase(Locale.ROOT); - if (normalized.indexOf(':') < 0) { - normalized = "minecraft:" + normalized; - } - if (!isSafeResourceKey(normalized)) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' has invalid tag '" + tag + "'."); - } - } - } - - private static boolean isSafeResourceKey(String key) { - if (!RESOURCE_KEY_PATTERN.matcher(key).matches()) { - return false; - } - int separator = key.indexOf(':'); - String[] segments = key.substring(separator + 1).split("/"); - for (String segment : segments) { - if (segment.equals("..")) { - return false; - } - } - return true; - } - - private static void validateCustomBiomeDerivativeSpawns(String biomeKey, - JSONObject derivative, - int derivativeIndex, - Function categoryResolver, - List blockingErrors) { - JSONArray spawns = derivative.optJSONArray("spawns"); - if (spawns == null) { - if (derivative.has("spawns") && !derivative.isNull("spawns")) { - String derivativeId = derivative.optString("id", "#" + derivativeIndex); - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' spawns must be an array."); - } - return; - } - String derivativeId = derivative.optString("id", "#" + derivativeIndex); - for (int spawnIndex = 0; spawnIndex < spawns.length(); spawnIndex++) { - JSONObject spawn = spawns.optJSONObject(spawnIndex); - if (spawn == null) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' has a non-object spawn at index " + spawnIndex + "."); - continue; - } - - String type = spawn.optString("type", "").trim().toLowerCase(Locale.ROOT); - if (type.isEmpty()) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' has a spawn without an entity type at index " + spawnIndex + "."); - continue; - } - String typeKey = type.indexOf(':') >= 0 ? type : "minecraft:" + type; - SpawnCategoryResolution resolution; - try { - resolution = categoryResolver == null ? null : categoryResolver.apply(typeKey); - } catch (Throwable e) { - IrisLogging.reportError("PackValidator failed to resolve spawn category for '" + typeKey + "'", e); - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' spawn category lookup failed for '" + typeKey + "': " + e.getMessage()); - continue; - } - if (resolution != null && !resolution.entityKnown()) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' spawn references unknown entity type '" + typeKey + "'."); - continue; - } - String expectedGroup = resolution == null ? null : resolution.category(); - String group = spawn.optString("group", "").trim(); - if (group.isEmpty()) { - if (expectedGroup != null && !expectedGroup.isBlank() - && !IrisBiomeCustomSpawnType.MISC.name().equalsIgnoreCase(expectedGroup)) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' spawn '" + typeKey + "' must declare group '" - + expectedGroup.toUpperCase(Locale.ROOT) + "'."); - } - continue; - } - - IrisBiomeCustomSpawnType configuredGroup; - try { - configuredGroup = IrisBiomeCustomSpawnType.valueOf(group); - } catch (IllegalArgumentException e) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' spawn '" + typeKey + "' declares unknown group '" + group + "'."); - continue; - } - - if (expectedGroup != null && !expectedGroup.isBlank() - && !configuredGroup.name().equalsIgnoreCase(expectedGroup)) { - blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId - + "' spawn '" + typeKey + "' declares group '" + configuredGroup.name() - + "' but the live entity registry requires '" + expectedGroup.toUpperCase(Locale.ROOT) + "'."); - } - } - } - - private static SpawnCategoryResolution resolveEntitySpawnCategory(String typeKey) { - if (!IrisPlatforms.isBound()) { - return null; - } - PlatformRegistries registries = IrisPlatforms.get().registries(); - if (registries == null) { - return null; - } - PlatformEntityType entityType = registries.entity(typeKey); - return entityType == null - ? SpawnCategoryResolution.unknown() - : SpawnCategoryResolution.known(entityType.spawnCategory()); - } - - private static void runContentKeyValidation(File packFolder, List warnings) { - try { - if (!IrisPlatforms.isBound()) { - return; - } - PlatformRegistries registries = IrisPlatforms.get().registries(); - if (registries == null) { - return; - } - List blockKeys = registries.blockKeys(); - List itemKeys = registries.itemKeys(); - List entityKeys = registries.entityKeys(); - if (blockKeys == null || blockKeys.isEmpty() || itemKeys == null || itemKeys.isEmpty() || entityKeys == null || entityKeys.isEmpty()) { - return; - } - - ReferencedContentKeys referenced = collectReferencedContentKeys(packFolder); - List errors = ContentKeyValidator.validate( - registries, referenced.blocks(), referenced.items(), referenced.entities()); - for (ContentKeyValidator.ContentKeyError error : errors) { - warnings.add(error.message()); - } - } catch (Throwable e) { - IrisLogging.reportError("PackValidator content-key validation failed for pack '" + packFolder.getName() + "'", e); - } - } - - private static ReferencedContentKeys collectReferencedContentKeys(File packFolder) { - Set blocks = new HashSet<>(); - Set items = new HashSet<>(); - Set entities = new HashSet<>(); - Set customBlocks = deriveRegistrantKeys(new File(packFolder, "blocks")); - - try (Stream stream = Files.walk(packFolder.toPath())) { - List files = stream.filter(Files::isRegularFile) - .filter(PackValidator::isScannableJsonPath) - .toList(); - for (Path path : files) { - String relative = packFolder.toPath().relativize(path).toString().replace(File.separatorChar, '/'); - boolean inLoot = relative.startsWith("loot/"); - boolean inEntities = relative.startsWith("entities/"); - JSONObject json; - try { - json = new JSONObject(Files.readString(path, StandardCharsets.UTF_8)); - } catch (Throwable ignored) { - continue; - } - collectFromNode(json, blocks, inLoot ? items : null, inEntities ? entities : null, customBlocks); - } - } catch (Throwable e) { - IrisLogging.reportError("PackValidator failed to walk pack for content-key extraction", e); - } - - return new ReferencedContentKeys(blocks, items, entities); - } - - private static void collectFromNode(Object node, Set blocks, Set items, Set entities, Set customBlocks) { - if (node instanceof JSONObject obj) { - for (String key : obj.keySet()) { - Object value = obj.get(key); - if (value instanceof String str) { - if ("block".equals(key)) { - addBlockRef(str, blocks, customBlocks); - } else if (items != null && "type".equals(key)) { - addSimpleRef(str, items); - } else if (entities != null && "type".equals(key)) { - addSimpleRef(str, entities); - } - } else { - collectFromNode(value, blocks, items, entities, customBlocks); - } - } - } else if (node instanceof JSONArray arr) { - for (int i = 0; i < arr.length(); i++) { - collectFromNode(arr.get(i), blocks, items, entities, customBlocks); - } - } - } - - private static void addBlockRef(String raw, Set blocks, Set customBlocks) { - String value = raw.trim().toLowerCase(Locale.ROOT); - int bracket = value.indexOf('['); - if (bracket >= 0) { - value = value.substring(0, bracket).trim(); - } - if (value.isEmpty() || customBlocks.contains(value)) { - return; - } - blocks.add(value); - } - - private static void addSimpleRef(String raw, Set target) { - String value = raw.trim().toLowerCase(Locale.ROOT); - if (!value.isEmpty()) { - target.add(value); - } - } - - private static Set deriveRegistrantKeys(File folder) { - Set keys = new HashSet<>(); - if (!folder.isDirectory()) { - return keys; - } - for (File file : listJsonRecursive(folder)) { - String key = deriveKey(folder, file); - if (key != null && !key.isBlank()) { - keys.add(key.toLowerCase(Locale.ROOT)); - } - } - return keys; - } - - private static Set deriveRegistrantKeysExact(File folder) { - Set keys = new HashSet<>(); - if (!folder.isDirectory()) { - return keys; - } - for (File file : listJsonRecursive(folder)) { - String key = deriveKey(folder, file); - if (key != null && !key.isBlank()) { - keys.add(key); - } - } - return keys; - } - - private static Set deriveObjectKeysExact(File folder) { - Set keys = new HashSet<>(); - if (!folder.isDirectory()) { - return keys; - } - try (Stream stream = Files.walk(folder.toPath())) { - stream.filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".iob")) - .forEach(path -> { - Path relative = folder.toPath().relativize(path); - String key = relative.toString().replace(File.separatorChar, '/'); - keys.add(key.substring(0, key.length() - ".iob".length())); - }); - } catch (IOException ignored) { - } - return keys; - } - - private record ReferencedContentKeys(Set blocks, Set items, Set entities) { - } - - private record DimensionVerticalBounds(long minimumY, long maximumY) { - } - - private record PlacementVerticalBounds(long minimumY, long maximumY, boolean underground) { - } - - record SpawnCategoryResolution(boolean entityKnown, String category) { - static SpawnCategoryResolution unknown() { - return new SpawnCategoryResolution(false, null); - } - - static SpawnCategoryResolution known(String category) { - return new SpawnCategoryResolution(true, category); - } - } - - private static void validateDimensions(File packFolder, File[] dimensionFiles, List blockingErrors, List warnings) { - File regionsFolder = new File(packFolder, "regions"); - File biomesFolder = new File(packFolder, "biomes"); - - for (File dimFile : dimensionFiles) { - String dimensionKey = stripExtension(dimFile.getName()); - JSONObject dimJson; - try { - dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable e) { - blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage()); - continue; - } - - validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors); - - JSONArray regionsArray = dimJson.optJSONArray("regions"); - if (regionsArray == null || regionsArray.length() == 0) { - blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions."); - continue; - } - - int resolvedRegions = 0; - for (int i = 0; i < regionsArray.length(); i++) { - String regionKey = regionsArray.optString(i, null); - if (regionKey == null || regionKey.isBlank()) { - warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + "."); - continue; - } - File regionFile = new File(regionsFolder, regionKey + ".json"); - if (!regionFile.isFile()) { - blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'."); - continue; - } - - JSONObject regionJson; - try { - regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable e) { - blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage()); - continue; - } - - int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings) - + countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings) - + countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings) - + countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings); - if (anyBiome == 0) { - blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes."); - } - resolvedRegions++; - } - - if (resolvedRegions == 0) { - blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions."); - } - } - } - - static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension, - List blockingErrors) { - if (!dimension.has("importedStructures")) { - return; - } - if (dimension.isNull("importedStructures")) { - blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object."); - return; - } - JSONObject policy = dimension.optJSONObject("importedStructures"); - if (policy == null) { - blockingErrors.add("Dimension '" + dimensionKey + "' importedStructures must be an object."); - return; - } - if (policy.has("mode")) { - blockingErrors.add("Dimension '" + dimensionKey - + "' importedStructures.mode is not supported. Native structures are enabled by default; list explicit denials in importedStructures.disabled."); - } - if (policy.has("enabled")) { - blockingErrors.add("Dimension '" + dimensionKey - + "' importedStructures.enabled is not supported. Native structures are enabled by default; list explicit denials in importedStructures.disabled."); - } - validateStructureKeyList(dimensionKey, policy, "disabled", blockingErrors); - JSONArray adjustments = policy.optJSONArray("adjustments"); - if (adjustments == null) { - if (policy.has("adjustments")) { - blockingErrors.add("Dimension '" + dimensionKey - + "' importedStructures.adjustments must be an array."); - } - return; - } - for (int index = 0; index < adjustments.length(); index++) { - JSONObject adjustment = adjustments.optJSONObject(index); - if (adjustment == null) { - blockingErrors.add("Dimension '" + dimensionKey - + "' importedStructures.adjustments has a non-object entry at index " + index + "."); - continue; - } - validateStructureKeyList(dimensionKey, adjustment, "match", blockingErrors); - validateAdjustmentYBand(dimensionKey, adjustment, index, blockingErrors); - validateNativeTerrain("Dimension '" + dimensionKey - + "' importedStructures.adjustments[" + index + "]", adjustment, blockingErrors); - } - } - - private static void validateAdjustmentYBand(String dimensionKey, JSONObject adjustment, int index, - List blockingErrors) { - if (!adjustment.has("yBand") || adjustment.opt("yBand") == JSONObject.NULL) { - return; - } - String path = "Dimension '" + dimensionKey - + "' importedStructures.adjustments[" + index + "].yBand"; - JSONObject band = adjustment.optJSONObject("yBand"); - if (band == null) { - blockingErrors.add(path + " must be an object."); - return; - } - validateOptionalIntegerRange(path, band, "min", -4064, 4064, blockingErrors); - validateOptionalIntegerRange(path, band, "max", -4064, 4064, blockingErrors); - } - - private static void validateStructureKeyList(String dimensionKey, JSONObject owner, String field, - List blockingErrors) { - if (!owner.has(field)) { - return; - } - JSONArray keys = owner.optJSONArray(field); - if (keys == null) { - blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '" - + field + "' must be an array."); - return; - } - for (int index = 0; index < keys.length(); index++) { - Object value = keys.opt(index); - if (!(value instanceof String key) || key.isBlank()) { - blockingErrors.add("Dimension '" + dimensionKey + "' structure policy field '" - + field + "' has a blank or non-string entry at index " + index + "."); - } - } - } - - private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List warnings) { - JSONArray arr = regionJson.optJSONArray(field); - if (arr == null) { - return 0; - } - int resolved = 0; - for (int i = 0; i < arr.length(); i++) { - String biomeKey = arr.optString(i, null); - if (biomeKey == null || biomeKey.isBlank()) { - continue; - } - File biomeFile = new File(biomesFolder, biomeKey + ".json"); - if (!biomeFile.isFile()) { - warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + "."); - continue; - } - resolved++; - } - return resolved; - } - - private static boolean isScannableJsonPath(Path path) { - String name = path.getFileName().toString(); - if (!name.endsWith(".json")) { - return false; - } - String str = path.toString().replace(File.separatorChar, '/'); - if (str.contains("/" + TRASH_ROOT + "/")) { - return false; - } - if (str.contains("/" + DATAPACK_IMPORTS + "/")) { - return false; - } - if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) { - return false; - } - if (str.contains("/" + INTERNAL_DATAPACKS + "/")) { - return false; - } - if (str.contains("/" + DATAPACKS_FOLDER + "/")) { - return false; - } - if (str.contains("/" + CACHE_FOLDER + "/")) { - return false; - } - if (str.contains("/" + OBJECTS_FOLDER + "/")) { - return false; - } - if (str.contains("/.iris/")) { - return false; - } - return true; - } - - private static List listJsonRecursive(File root) { - List out = new ArrayList<>(); - try (Stream stream = Files.walk(root.toPath())) { - stream.filter(Files::isRegularFile) - .filter(p -> p.getFileName().toString().endsWith(".json")) - .forEach(p -> out.add(p.toFile())); - } catch (Throwable ignored) { - } - return out; - } - - private static String deriveKey(File resourceFolder, File resourceFile) { - Path relative = resourceFolder.toPath().relativize(resourceFile.toPath()); - String str = relative.toString().replace(File.separatorChar, '/'); - if (!str.endsWith(".json")) { - return null; - } - return str.substring(0, str.length() - ".json".length()); - } - - private static String stripExtension(String name) { - int dot = name.lastIndexOf('.'); - return dot <= 0 ? name : name.substring(0, dot); - } - - public static Set listReferencedKeysFromCorpus(String corpus) { - Set keys = new HashSet<>(); - if (corpus == null) { - return keys; - } - int i = 0; - while (i < corpus.length()) { - int start = corpus.indexOf('"', i); - if (start < 0) { - break; - } - int end = corpus.indexOf('"', start + 1); - if (end < 0) { - break; - } - keys.add(corpus.substring(start + 1, end)); - i = end + 1; - } - return keys; - } } 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 a4908dcf3..6f22d05e1 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 @@ -118,7 +118,9 @@ public class IrisPregenerator { generatedLast.set(generated.get()); if (secondCached == 0 || secondGenerated != 0) { chunksPerSecond.put(secondGenerated); - chunksPerSecondHistory.add((int) secondGenerated); + synchronized (chunksPerSecondHistory) { + chunksPerSecondHistory.add((int) secondGenerated); + } } if (minuteLatch.flip()) { @@ -222,7 +224,13 @@ public class IrisPregenerator { logIncompleteCompletion(p); } if (benchmarking != null) { - benchmarking.finishedBenchmark(chunksPerSecondHistory); + benchmarking.finishedBenchmark(snapshotChunksPerSecondHistory()); + } + } + + private KList snapshotChunksPerSecondHistory() { + synchronized (chunksPerSecondHistory) { + return new KList<>(chunksPerSecondHistory); } } @@ -264,18 +272,31 @@ public class IrisPregenerator { } private void shutdown() { - listener.onSaving(); - generator.close(); - ticker.interrupt(); - listener.onClose(); - IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); - if (protocolServer != null) { - long total = totalChunks.get(); - protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total); - } - Mantle mantle = getMantle(); - if (mantle != null) { - reclaimTectonicPlates(mantle); + shutdownStep("saving", listener::onSaving); + shutdownStep("generator", generator::close); + shutdownStep("ticker", ticker::interrupt); + shutdownStep("listener", listener::onClose); + shutdownStep("protocol", () -> { + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer != null) { + long total = totalChunks.get(); + protocolServer.pregenEnd(jobId, total > 0 && generated.get() >= total); + } + }); + shutdownStep("mantle", () -> { + Mantle mantle = getMantle(); + if (mantle != null) { + reclaimTectonicPlates(mantle); + } + }); + } + + private void shutdownStep(String step, Runnable action) { + try { + action.run(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Pregen shutdown step " + step + " failed: " + e.getClass().getSimpleName() + ": " + e.getMessage()); } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java index 9f7f78f58..87cd9d45f 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java @@ -22,6 +22,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.M; +import java.util.function.BooleanSupplier; import java.util.function.Supplier; public final class PregenMantleBackpressure { @@ -31,14 +32,20 @@ public final class PregenMantleBackpressure { private final long timeoutMs; private final Runnable onBudgetTimeout; private final Supplier diagnostics; + private final BooleanSupplier cancelled; public PregenMantleBackpressure(Supplier mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier diagnostics) { + this(mantleSupplier, maxResidentTectonicPlates, waitMs, timeoutMs, onBudgetTimeout, diagnostics, () -> false); + } + + public PregenMantleBackpressure(Supplier mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier diagnostics, BooleanSupplier cancelled) { this.mantleSupplier = mantleSupplier; this.maxResidentTectonicPlates = maxResidentTectonicPlates; this.waitMs = waitMs; this.timeoutMs = timeoutMs; this.onBudgetTimeout = onBudgetTimeout; this.diagnostics = diagnostics; + this.cancelled = cancelled; } public void apply() { @@ -65,6 +72,10 @@ public final class PregenMantleBackpressure { long waitStart = M.ms(); long lastLog = 0L; while (mantle.getLoadedRegionCount() > hardCap) { + if (isCancelled()) { + return; + } + int freed; int resident; try { @@ -106,8 +117,13 @@ public final class PregenMantleBackpressure { public void awaitHeapHeadroom() { Mantle mantle = resolveMantle(); + long waitStart = M.ms(); long lastLog = 0L; while (MantleHeapPressure.overHighWater()) { + if (isCancelled()) { + return; + } + try { if (mantle != null && mantle.getLoadedRegionCount() > maxResidentTectonicPlates) { mantle.trim(0L, 0); @@ -121,6 +137,15 @@ public final class PregenMantleBackpressure { MantleHeapPressure.requestPanicReclaim(); } + long elapsed = M.ms() - waitStart; + if (elapsed >= timeoutMs) { + IrisLogging.warn("Pregen heap pressure wait exceeded " + timeoutMs + "ms at " + + Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; proceeding to avoid deadlock. " + + diagnostics.get()); + onBudgetTimeout.run(); + return; + } + long logNow = M.ms(); if (logNow - lastLog >= 5_000L) { lastLog = logNow; @@ -138,6 +163,18 @@ public final class PregenMantleBackpressure { } } + private boolean isCancelled() { + if (Thread.currentThread().isInterrupted()) { + return true; + } + + try { + return cancelled.getAsBoolean(); + } catch (Throwable ignored) { + return false; + } + } + private Mantle resolveMantle() { try { return mantleSupplier.get(); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java index ca3c98971..381b417e1 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java @@ -178,12 +178,13 @@ public class PregenCacheImpl implements PregenCache { return; } - File file = fileForPlate(plate.x, plate.z); + File file = null; try { + file = fileForPlate(plate.x, plate.z); IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write); plate.dirty = false; - } catch (IOException e) { - IrisLogging.error("Failed to write preen cache " + file); + } catch (Throwable e) { + IrisLogging.error("Failed to write pregen cache " + (file != null ? file : "c." + plate.x + "." + plate.z)); e.printStackTrace(); IrisLogging.reportError(e); } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index dbec3c8cf..2ad172667 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -51,6 +51,8 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -58,6 +60,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); private static final int ADAPTIVE_TIMEOUT_STEP = 3; private static final int ADAPTIVE_RECOVERY_INTERVAL = 8; + private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L; + private static final long FLUSH_TIMEOUT_SECONDS = 120L; private final World world; private final IrisRuntimeSchedulerMode runtimeSchedulerMode; private final IrisPaperLikeBackendMode paperLikeBackendMode; @@ -97,6 +101,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final AtomicLong completed = new AtomicLong(); private final AtomicLong failed = new AtomicLong(); private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); + private final AtomicBoolean closing = new AtomicBoolean(); private final Object permitMonitor = new Object(); private volatile Engine metricsEngine; private volatile Mantle cachedMantle; @@ -169,7 +174,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { pregen.getMantleBackpressureWaitMs(), pregen.getMantleBackpressureTimeoutMs(), this::lowerAdaptiveInFlightLimit, - this::metricsSnapshot); + this::metricsSnapshot, + this::isCancelled); } public static AsyncPregenMethod strictSerial(World world) { @@ -408,50 +414,92 @@ public class AsyncPregenMethod implements PregeneratorMethod { return; } - try { - J.sfut(() -> { - for (Long rk : keys) { - if (!evictedRegions.add(rk)) { - continue; - } - - regionPending.remove(rk); - Queue chunks = regionChunks.remove(rk); - if (chunks == null) { - continue; - } - - for (Chunk chunk : chunks) { - if (chunk != null) { - unloadChunkSafely(chunk.getX(), chunk.getZ()); - } - } + CompletableFuture flush = J.sfut(() -> { + for (Long rk : keys) { + if (!evictedRegions.add(rk)) { + continue; } - world.save(); - INMS.get().flushChunkIO(world); - }).get(); + regionPending.remove(rk); + Queue chunks = regionChunks.remove(rk); + if (chunks == null) { + continue; + } + + for (Chunk chunk : chunks) { + if (chunk != null) { + unloadChunkSafely(chunk.getX(), chunk.getZ()); + } + } + } + + world.save(); + INMS.get().flushChunkIO(world); + }); + + try { + flush.get(FLUSH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + IrisLogging.warn("Interrupted while flushing pregen chunks for " + world.getName() + "."); + } catch (TimeoutException e) { + IrisLogging.warn("Pregen chunk flush for " + world.getName() + " did not finish in " + FLUSH_TIMEOUT_SECONDS + + "s, continuing shutdown. " + keys.size() + " region(s) queued."); } catch (Throwable e) { IrisLogging.reportError(e); } } private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) { - Throwable root = throwable; - while (root.getCause() != null) { - root = root.getCause(); + try { + Throwable root = throwable; + while (root.getCause() != null) { + root = root.getCause(); + } + + if (root instanceof TimeoutException) { + onTimeout(x, z); + } else { + IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot()); + } + + IrisLogging.reportError(throwable); + } catch (Throwable e) { + e.printStackTrace(); } - if (root instanceof java.util.concurrent.TimeoutException) { - onTimeout(x, z); - } else { - IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot()); - } - - IrisLogging.reportError(throwable); return null; } + private void completeChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) { + boolean success = false; + try { + if (throwable != null) { + onChunkFutureFailure(x, z, throwable); + onChunkFailedToLoad(x, z); + listener.onChunkFailed(x, z); + } else if (chunk == null) { + onChunkFailedToLoad(x, z); + listener.onChunkFailed(x, z); + } else { + listener.onChunkGenerated(x, z); + cleanupMantleChunk(x, z); + listener.onChunkCleaned(x, z); + onChunkCompleted(x, z, chunk); + success = true; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } finally { + try { + markFinished(success); + } finally { + semaphore.release(); + } + } + } + private void onTimeout(int x, int z) { int streak = timeoutStreak.incrementAndGet(); if (streak % ADAPTIVE_TIMEOUT_STEP == 0) { @@ -707,10 +755,36 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void close() { - semaphore.acquireUninterruptibly(threads); - flushAllRemainingChunks(); - executor.shutdown(); - resetWorkerThreads(); + closing.set(true); + notifyPermitWaiters(); + + // A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk. + boolean interrupted = Thread.interrupted(); + try { + boolean drained = false; + try { + drained = semaphore.tryAcquire(threads, CLOSE_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + interrupted = true; + } + + if (!drained) { + IrisLogging.warn("Async pregen close did not drain in " + CLOSE_DRAIN_TIMEOUT_SECONDS + + "s, continuing degraded. " + metricsSnapshot()); + } + + flushAllRemainingChunks(); + executor.shutdown(); + resetWorkerThreads(); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + private boolean isCancelled() { + return closing.get() || Thread.currentThread().isInterrupted(); } @Override @@ -732,10 +806,18 @@ public class AsyncPregenMethod implements PregeneratorMethod { listener.onChunkGenerating(x, z); backpressure.enforceMantleBudget(); backpressure.awaitHeapHeadroom(); + if (isCancelled()) { + return; + } + try { long waitStart = M.ms(); synchronized (permitMonitor) { while (inFlight.get() >= adaptiveInFlightLimit.get()) { + if (isCancelled()) { + return; + } + permitMonitor.wait(500L); } } @@ -746,6 +828,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { long permitWaitStart = M.ms(); while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) { + if (isCancelled()) { + return; + } } long permitWait = Math.max(0L, M.ms() - permitWaitStart); if (permitWait > 0L) { @@ -921,48 +1006,24 @@ public class AsyncPregenMethod implements PregeneratorMethod { try { requestChunkAsync(x, z) .orTimeout(timeoutSeconds, TimeUnit.SECONDS) - .whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)); + .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); return; } catch (Throwable ignored) { } - if (!J.runRegion(world, x, z, () -> requestChunkAsync(x, z) - .orTimeout(timeoutSeconds, TimeUnit.SECONDS) - .whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)))) { - markFinished(false); - semaphore.release(); - listener.onChunkFailed(x, z); - IrisLogging.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot()); - } - } - - private void completeFoliaChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) { - boolean success = false; - try { - if (throwable != null) { - onChunkFutureFailure(x, z, throwable); - onChunkFailedToLoad(x, z); - listener.onChunkFailed(x, z); - return; + Runnable regionTask = () -> { + try { + requestChunkAsync(x, z) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) + .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); } + }; - if (chunk == null) { - onChunkFailedToLoad(x, z); - listener.onChunkFailed(x, z); - return; - } - - listener.onChunkGenerated(x, z); - cleanupMantleChunk(x, z); - listener.onChunkCleaned(x, z); - onChunkCompleted(x, z, chunk); - success = true; - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } finally { - markFinished(success); - semaphore.release(); + if (!J.runRegion(world, x, z, regionTask)) { + completeChunk(x, z, listener, null, + new IllegalStateException("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot())); } } } @@ -971,35 +1032,23 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final ExecutorService service = new MultiBurst("Iris Async Pregen"); public void generate(int x, int z, PregenListener listener) { - service.submit(() -> { - boolean success = false; - try { - Chunk i = requestChunkAsync(x, z) - .orTimeout(timeoutSeconds, TimeUnit.SECONDS) - .exceptionally(e -> onChunkFutureFailure(x, z, e)) - .get(); - - if (i == null) { - onChunkFailedToLoad(x, z); - listener.onChunkFailed(x, z); - return; + try { + service.submit(() -> { + try { + Chunk i = requestChunkAsync(x, z) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) + .get(); + completeChunk(x, z, listener, i, null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + completeChunk(x, z, listener, null, e); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); } - - listener.onChunkGenerated(x, z); - cleanupMantleChunk(x, z); - listener.onChunkCleaned(x, z); - onChunkCompleted(x, z, i); - success = true; - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } finally { - markFinished(success); - semaphore.release(); - } - }); + }); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); + } } @Override @@ -1011,28 +1060,13 @@ public class AsyncPregenMethod implements PregeneratorMethod { private class TicketExecutor implements Executor { @Override public void generate(int x, int z, PregenListener listener) { - requestChunkAsync(x, z) - .orTimeout(timeoutSeconds, TimeUnit.SECONDS) - .exceptionally(e -> onChunkFutureFailure(x, z, e)) - .thenAccept(i -> { - boolean success = false; - try { - if (i == null) { - onChunkFailedToLoad(x, z); - listener.onChunkFailed(x, z); - return; - } - - listener.onChunkGenerated(x, z); - cleanupMantleChunk(x, z); - listener.onChunkCleaned(x, z); - onChunkCompleted(x, z, i); - success = true; - } finally { - markFinished(success); - semaphore.release(); - } - }); + try { + requestChunkAsync(x, z) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) + .whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable)); + } catch (Throwable e) { + completeChunk(x, z, listener, null, e); + } } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/DummyPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/DummyPregenMethod.java deleted file mode 100644 index 876b240c7..000000000 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/DummyPregenMethod.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.pregenerator.methods; - -import art.arcane.iris.core.pregenerator.PregenListener; -import art.arcane.iris.core.pregenerator.PregeneratorMethod; -import art.arcane.volmlib.util.mantle.runtime.Mantle; - -public class DummyPregenMethod implements PregeneratorMethod { - @Override - public void init() { - - } - - @Override - public void close() { - - } - - @Override - public String getMethod(int x, int z) { - return "Dummy"; - } - - @Override - public void save() { - - } - - @Override - public boolean supportsRegions(int x, int z, PregenListener listener) { - return false; - } - - @Override - public void generateRegion(int x, int z, PregenListener listener) { - - } - - @Override - public void generateChunk(int x, int z, PregenListener listener) { - - } - - @Override - public Mantle getMantle() { - return null; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java index b98fb1a6b..048930171 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -41,10 +41,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; 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.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class MedievalPregenMethod implements PregeneratorMethod { + private static final long CHUNK_WAIT_TIMEOUT_SECONDS = 60L; + private static final long UNLOAD_TIMEOUT_SECONDS = 120L; private final World world; private final KList> futures; private final Map lastUse; @@ -105,9 +109,14 @@ public class MedievalPregenMethod implements PregeneratorMethod { private void waitForChunks() { for (CompletableFuture i : futures) { try { - i.get(); + i.get(CHUNK_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (TimeoutException e) { + IrisLogging.warn("Medieval pregen chunk did not finish in " + CHUNK_WAIT_TIMEOUT_SECONDS + "s, abandoning it."); } catch (Throwable e) { - e.printStackTrace(); + IrisLogging.reportError(e); } } @@ -120,26 +129,32 @@ public class MedievalPregenMethod implements PregeneratorMethod { return; } - try { - J.sfut(() -> { - if (world == null) { - IrisLogging.warn("World was null somehow..."); - return; - } + CompletableFuture unload = J.sfut(() -> { + if (world == null) { + IrisLogging.warn("World was null somehow..."); + return; + } - for (Chunk i : new ArrayList<>(lastUse.keySet())) { - Long lastUseTime = lastUse.get(i); - if (lastUseTime != null && M.ms() - lastUseTime >= 10) { - i.unload(); - lastUse.remove(i); - } + for (Chunk i : new ArrayList<>(lastUse.keySet())) { + Long lastUseTime = lastUse.get(i); + if (lastUseTime != null && M.ms() - lastUseTime >= 10) { + i.unload(); + lastUse.remove(i); } - if (saveWorld) { - world.save(); - } - }).get(); + } + if (saveWorld) { + world.save(); + } + }); + + try { + unload.get(UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TimeoutException e) { + IrisLogging.warn("Medieval pregen chunk unload did not finish in " + UNLOAD_TIMEOUT_SECONDS + "s, continuing."); } catch (Throwable e) { - e.printStackTrace(); + IrisLogging.reportError(e); } } @@ -150,11 +165,19 @@ public class MedievalPregenMethod implements PregeneratorMethod { @Override public void close() { - waitForChunks(); - if (prefetchPool != null) { - prefetchPool.shutdownNow(); + // A stop request interrupts the pregen worker; shield the drain and save so chunks still hit disk. + boolean interrupted = Thread.interrupted(); + try { + waitForChunks(); + if (prefetchPool != null) { + prefetchPool.shutdownNow(); + } + unloadAndSaveAllChunks(true); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } } - unloadAndSaveAllChunks(true); } @Override @@ -212,22 +235,26 @@ public class MedievalPregenMethod implements PregeneratorMethod { try { prefetchPool.submit(() -> { try { - prefetchMantle(engine, x, z); - } catch (Throwable e) { - if (prefetchDisabled.compareAndSet(false, true)) { - IrisLogging.warn("Mantle prefetch failed at chunk " + x + "," + z + "; disabling prefetch for this pregen."); - IrisLogging.reportError(e); + try { + prefetchMantle(engine, x, z); + } catch (Throwable e) { + if (prefetchDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Mantle prefetch failed at chunk " + x + "," + z + "; disabling prefetch for this pregen."); + IrisLogging.reportError(e); + } } - } - CompletableFuture chunkFuture = runChunkLoad(x, z, listener); - chunkFuture.whenComplete((r, err) -> { - if (err != null) { - aggregate.completeExceptionally(err); - } else { - aggregate.complete(null); - } - }); + CompletableFuture chunkFuture = runChunkLoad(x, z, listener); + chunkFuture.whenComplete((r, err) -> { + if (err != null) { + aggregate.completeExceptionally(err); + } else { + aggregate.complete(null); + } + }); + } catch (Throwable e) { + aggregate.completeExceptionally(e); + } }); } catch (Throwable rejected) { if (prefetchDisabled.compareAndSet(false, true)) { @@ -268,7 +295,7 @@ public class MedievalPregenMethod implements PregeneratorMethod { } try { - generateChunkSync(x, z, listener).get(); + generateChunkSync(x, z, listener).get(CHUNK_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); future.complete(null); } catch (Throwable fallbackError) { future.completeExceptionally(fallbackError); diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java b/core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java new file mode 100644 index 000000000..0404d824a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisCodeWorkspace.java @@ -0,0 +1,298 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.dom4j.Document; +import org.dom4j.Element; + +import java.awt.Desktop; +import java.awt.GraphicsEnvironment; +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; + +@SuppressWarnings("ALL") +public class IrisCodeWorkspace { + private final IrisProject project; + + public IrisCodeWorkspace(IrisProject project) { + this.project = project; + } + + public void openVSCode(VolmitSender sender) { + + IrisDimension d = IrisData.loadAnyDimension(project.getName(), null); + J.attemptAsync(() -> + { + try { + if (d == null) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_LOAD_DIMENSION, MessageArgument.untrusted("value", String.valueOf(project.getName())))); + return; + } + + if (d.getLoader() == null) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_GET_DIMENSION_LOADER)); + return; + } + File f = d.getLoader().getDataFolder(); + + if (!doOpenVSCode(f)) { + File ff = new File(d.getLoader().getDataFolder(), d.getLoadKey() + ".code-workspace"); + IrisLogging.warn("Project missing code-workspace: " + ff.getAbsolutePath() + " Re-creating code workspace."); + + try { + IO.writeAll(ff, createCodeWorkspaceConfig(false)); + } catch (IOException e1) { + IrisLogging.reportError(e1); + e1.printStackTrace(); + } + if (!doOpenVSCode(f)) { + IrisLogging.warn("Tried creating code workspace but failed a second time. Your project is likely corrupt."); + } + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + }); + } + + private boolean doOpenVSCode(File f) throws IOException { + boolean foundWork = false; + for (File i : Objects.requireNonNull(f.listFiles())) { + if (i.getName().endsWith(".code-workspace")) { + foundWork = true; + + if (IrisSettings.get().getStudio().isOpenVSCode()) { + if (!GraphicsEnvironment.isHeadless()) { + IrisLogging.msg("Opening VSCode. You may see the output from VSCode."); + IrisLogging.msg("VSCode output always starts with: '(node:#####) electron'"); + Thread launcherThread = new Thread(() -> { + try { + Desktop.getDesktop().open(i); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }, "Iris-VSCode-Launcher"); + launcherThread.setDaemon(true); + launcherThread.start(); + } + } + + break; + } + } + return foundWork; + } + + public File getCodeWorkspaceFile() { + return new File(project.getPath(), project.getName() + ".code-workspace"); + } + + public boolean updateWorkspace() { + project.getPath().mkdirs(); + File ws = getCodeWorkspaceFile(); + + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + JSONObject j = createCodeWorkspaceConfig(); + IO.writeAll(ws, j.toString(4)); + p.end(); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!"); + ws.delete(); + try { + IO.writeAll(ws, createCodeWorkspaceConfig()); + } catch (IOException e1) { + IrisLogging.reportError(e1); + e1.printStackTrace(); + } + } + + return false; + } + + public JSONObject createCodeWorkspaceConfig() { + return createCodeWorkspaceConfig(true); + } + + private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) { + JSONObject ws = new JSONObject(); + JSONArray folders = new JSONArray(); + JSONObject folder = new JSONObject(); + folder.put("path", "."); + folders.put(folder); + ws.put("folders", folders); + JSONObject settings = new JSONObject(); + settings.put("workbench.colorTheme", "Monokai"); + settings.put("workbench.preferredDarkColorTheme", "Solarized Dark"); + settings.put("workbench.tips.enabled", false); + settings.put("workbench.tree.indent", 24); + settings.put("files.autoSave", "onFocusChange"); + JSONObject jc = new JSONObject(); + jc.put("editor.autoIndent", "brackets"); + jc.put("editor.acceptSuggestionOnEnter", "smart"); + jc.put("editor.cursorSmoothCaretAnimation", true); + jc.put("editor.dragAndDrop", false); + jc.put("files.trimTrailingWhitespace", true); + jc.put("diffEditor.ignoreTrimWhitespace", true); + jc.put("files.trimFinalNewlines", true); + jc.put("editor.suggest.showKeywords", false); + jc.put("editor.suggest.showSnippets", false); + jc.put("editor.suggest.showWords", false); + JSONObject st = new JSONObject(); + st.put("strings", true); + jc.put("editor.quickSuggestions", st); + jc.put("editor.suggest.insertMode", "replace"); + settings.put("[json]", jc); + settings.put("json.maxItemsComputed", 30000); + JSONArray schemas = new JSONArray(); + IrisData dm = null; + if (includeSchemas) { + dm = IrisData.get(project.getPath()); + for (ResourceLoader r : dm.getLoaders().v()) { + if (r.supportsSchemas()) { + schemas.put(r.buildSchema()); + } + } + + for (Class i : dm.resolveSnippets()) { + try { + String snipType = i.getDeclaredAnnotation(Snippet.class).value(); + JSONObject o = new JSONObject(); + KList fm = new KList<>(); + + for (int g = 1; g < 8; g++) { + fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json"); + } + + o.put("fileMatch", new JSONArray(fm.toArray())); + o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); + schemas.put(o); + IrisData snippetData = dm; + File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); + J.attemptAsync(() -> { + try { + IO.writeAll(a, new SchemaBuilder(i, snippetData).construct().toString(4)); + } catch (Throwable e) { + e.printStackTrace(); + } + }); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + settings.put("json.schemas", schemas); + ws.put("settings", settings); + + if (!includeSchemas) { + return ws; + } + + File schemasFile = new File(project.getPath(), ".idea" + File.separator + "jsonSchemas.xml"); + Document doc = IO.read(schemasFile); + Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']"); + if (mappings == null) { + mappings = doc.getRootElement() + .addElement("component") + .addAttribute("name", "JsonSchemaMappingsProjectConfiguration"); + } + + Element state = (Element) mappings.selectSingleNode("state"); + if (state == null) state = mappings.addElement("state"); + + Element map = (Element) state.selectSingleNode("map"); + if (map == null) map = state.addElement("map"); + var schemaMap = new KMap(); + schemas.forEach(element -> { + if (!(element instanceof JSONObject obj)) + return; + + String url = obj.getString("url"); + String dir = obj.getJSONArray("fileMatch").getString(0); + schemaMap.put(url, dir.substring(1, dir.indexOf("/*"))); + }); + + map.selectNodes("entry/value/SchemaInfo/option[@name='relativePathToSchema']") + .stream() + .map(node -> node.valueOf("@value")) + .forEach(schemaMap::remove); + + var ideaSchemas = map; + schemaMap.forEach((url, dir) -> { + var genName = UUID.randomUUID().toString(); + + var info = ideaSchemas.addElement("entry") + .addAttribute("key", genName) + .addElement("value") + .addElement("SchemaInfo"); + info.addElement("option") + .addAttribute("name", "generatedName") + .addAttribute("value", genName); + info.addElement("option") + .addAttribute("name", "name") + .addAttribute("value", dir); + info.addElement("option") + .addAttribute("name", "relativePathToSchema") + .addAttribute("value", url); + + + var item = info.addElement("option") + .addAttribute("name", "patterns") + .addElement("list") + .addElement("Item"); + item.addElement("option") + .addAttribute("name", "directory") + .addAttribute("value", "true"); + item.addElement("option") + .addAttribute("name", "path") + .addAttribute("value", dir); + item.addElement("option") + .addAttribute("name", "mappingKind") + .addAttribute("value", "Directory"); + }); + if (!schemaMap.isEmpty()) { + IO.write(schemasFile, doc); + } + return ws; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java b/core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java new file mode 100644 index 000000000..9f48f1963 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java @@ -0,0 +1,252 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import com.google.gson.Gson; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.pack.StructurePackageClosure; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockData; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisEntity; +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisLootTable; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.object.IrisStructurePlacement; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.scheduling.ChronoLatch; +import art.arcane.volmlib.util.scheduling.O; +import org.zeroturnaround.zip.ZipUtil; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; + +@SuppressWarnings("ALL") +public class IrisPackageCompiler { + private final IrisProject project; + + public IrisPackageCompiler(IrisProject project) { + this.project = project; + } + + public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) { + String dimm = project.getName(); + IrisData dm = IrisData.get(project.getPath()); + IrisDimension dimension = dm.getDimensionLoader().load(dimm); + File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey()); + IO.delete(folder); + if (folder.exists()) { + throw new IllegalStateException("Failed to clear structure package staging folder " + folder.getAbsolutePath()); + } + if (!folder.mkdirs() && !folder.isDirectory()) { + throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath()); + } + IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : "")); + KSet regions = new KSet<>(); + KSet biomes = new KSet<>(); + KSet entities = new KSet<>(); + KSet spawners = new KSet<>(); + KSet generators = new KSet<>(); + KSet loot = new KSet<>(); + KSet blocks = new KSet<>(); + + for (String i : dm.getBlockLoader().getPossibleKeys()) { + blocks.add(dm.getBlockLoader().load(i)); + } + + dimension.getRegions().forEach((i) -> regions.add(dm.getRegionLoader().load(i))); + dimension.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))); + regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm))); + regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)))); + regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)))); + dimension.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))); + biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm)))); + biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)))); + biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)))); + collectSpawnerEntityKeys(spawners).forEach((i) -> entities.add(dm.getEntityLoader().load(i))); + Set structureKeys = new LinkedHashSet<>(); + collectStructureKeys(structureKeys, dimension.getStructures()); + regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures())); + biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures())); + KMap renameObjects = new KMap<>(); + String a; + StringBuilder b = new StringBuilder(); + StringBuilder c = new StringBuilder(); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS)); + + for (IrisBiome i : biomes) { + for (IrisObjectPlacement j : i.getObjects()) { + b.append(j.hashCode()); + KList newNames = new KList<>(); + + for (String k : j.getPlace()) { + if (renameObjects.containsKey(k)) { + newNames.add(renameObjects.get(k)); + continue; + } + + String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", ""); + b.append(name); + newNames.add(name); + renameObjects.put(k, name); + } + + j.setPlace(newNames); + } + } + + KMap> lookupObjects = renameObjects.flip(); + StringBuilder gb = new StringBuilder(); + ChronoLatch cl = new ChronoLatch(1000); + O ggg = new O<>(); + ggg.set(0); + biomes.forEach((i) -> i.getObjects().forEach((j) -> j.getPlace().forEach((k) -> + { + try { + File f = dm.getObjectLoader().findFile(lookupObjects.get(k).get(0)); + IO.copyFile(f, new File(folder, "objects/" + k + ".iob")); + gb.append(IO.hash(f)); + ggg.set(ggg.get() + 1); + + if (cl.flip()) { + int g = ggg.get(); + ggg.set(0); + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g)))); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + }))); + + b.append(IO.hash(gb.toString())); + c.append(IO.hash(b.toString())); + b = new StringBuilder(); + + IrisLogging.info("Writing Dimensional Scaffold"); + + try { + StructurePackageClosure structureClosure = StructurePackageClosure.collect(project.getPath(), structureKeys); + if (!structureClosure.isValid()) { + throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors())); + } + b.append(structureClosure.writeTo(folder, minify)); + a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + + for (IrisGenerator i : generators) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "generators/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + c.append(IO.hash(b.toString())); + b = new StringBuilder(); + + for (IrisRegion i : regions) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "regions/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisBlockData i : blocks) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "blocks/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisBiome i : biomes) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "biomes/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisEntity i : entities) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "entities/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + for (IrisLootTable i : loot) { + a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); + IO.writeAll(new File(folder, "loot/" + i.getLoadKey() + ".json"), a); + b.append(IO.hash(a)); + } + + c.append(IO.hash(b.toString())); + String finalHash = IO.hash(c.toString()); + JSONObject meta = new JSONObject(); + meta.put("hash", finalHash); + meta.put("time", M.ms()); + meta.put("version", dimension.getVersion()); + IO.writeAll(new File(folder, "package.json"), meta.toString(minify ? 0 : 4)); + File p = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey() + ".iris"); + IrisLogging.info("Compressing Package"); + ZipUtil.pack(folder, p, 9); + IO.delete(folder); + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_PACKAGE_COMPILED)); + return p; + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED)); + return null; + } + + static KSet collectSpawnerEntityKeys(KSet spawners) { + KSet entityKeys = new KSet<>(); + for (IrisSpawner spawner : spawners) { + spawner.getSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity())); + spawner.getInitialSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity())); + } + return entityKeys; + } + + private static void collectStructureKeys(Set keys, KList placements) { + if (placements == null) { + return; + } + for (IrisStructurePlacement placement : placements) { + if (placement == null || placement.getStructures() == null) { + continue; + } + for (String structureKey : placement.getStructures()) { + keys.add(structureKey); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index 905a94484..0cadae095 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -18,85 +18,26 @@ package art.arcane.iris.core.project; -import com.google.gson.Gson; import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.IrisRegistrant; -import art.arcane.iris.core.loader.ResourceLoader; -import art.arcane.iris.core.pack.StructurePackageClosure; import art.arcane.iris.core.runtime.StudioOpenCoordinator; import art.arcane.iris.core.tools.IrisToolbelt; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBlockData; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisEntity; -import art.arcane.iris.engine.object.IrisGenerator; -import art.arcane.iris.engine.object.IrisLootTable; -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.IrisSpawner; -import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.platform.PlatformChunkGenerator; -import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.exceptions.IrisException; -import art.arcane.volmlib.util.hud.HudPriority; -import art.arcane.volmlib.util.hud.HudSlotClaim; -import art.arcane.volmlib.util.hud.HudSlotRequest; -import art.arcane.volmlib.util.hud.HudSurface; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.io.IO; -import art.arcane.volmlib.util.json.JSONArray; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.common.plugin.VolmitSender; -import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.iris.util.common.scheduling.J; -import art.arcane.volmlib.util.scheduling.O; -import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import art.arcane.iris.util.common.scheduling.jobs.Job; -import art.arcane.iris.util.common.scheduling.jobs.JobCollection; -import art.arcane.iris.util.common.scheduling.jobs.ParallelQueueJob; import lombok.Data; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.World; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; -import org.dom4j.Document; -import org.dom4j.Element; -import org.zeroturnaround.zip.ZipUtil; -import java.awt.Desktop; -import java.awt.GraphicsEnvironment; import java.io.File; -import java.io.IOException; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; import java.util.concurrent.CompletableFuture; 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.function.Consumer; import art.arcane.iris.core.localization.BukkitRuntimeMessages; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.RuntimeProgressMessages; -import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.volmlib.util.localization.MessageArgument; @SuppressWarnings("ALL") @Data @@ -110,62 +51,6 @@ public class IrisProject { this.name = path.getName(); } - public static int clean(VolmitSender s, File clean) { - int c = 0; - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - c += clean(s, i); - } - } else if (clean.getName().endsWith(".json")) { - try { - clean(clean); - } catch (Throwable e) { - IrisLogging.reportError(e); - IrisLogging.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!"); - } - - c++; - } - - return c; - } - - public static void clean(File clean) throws IOException { - JSONObject obj = new JSONObject(IO.readAll(clean)); - fixBlocks(obj, clean); - - IO.writeAll(clean, obj.toString(4)); - } - - public static void fixBlocks(JSONObject obj, File f) { - for (String i : obj.keySet()) { - Object o = obj.get(i); - - if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { - obj.put(i, "minecraft:" + o); - IrisLogging.debug("Updated Block Key: " + o + " to " + obj.getString(i) + " in " + f.getPath()); - } - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o, f); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o, f); - } - } - } - - public static void fixBlocks(JSONArray obj, File f) { - for (int i = 0; i < obj.length(); i++) { - Object o = obj.get(i); - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o, f); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o, f); - } - } - } - public boolean isOpen() { return activeProvider != null; } @@ -194,72 +79,6 @@ public class IrisProject { }); } - public void openVSCode(VolmitSender sender) { - - IrisDimension d = IrisData.loadAnyDimension(getName(), null); - J.attemptAsync(() -> - { - try { - if (d == null) { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_LOAD_DIMENSION, MessageArgument.untrusted("value", String.valueOf(getName())))); - return; - } - - if (d.getLoader() == null) { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_COULD_NOT_GET_DIMENSION_LOADER)); - return; - } - File f = d.getLoader().getDataFolder(); - - if (!doOpenVSCode(f)) { - File ff = new File(d.getLoader().getDataFolder(), d.getLoadKey() + ".code-workspace"); - IrisLogging.warn("Project missing code-workspace: " + ff.getAbsolutePath() + " Re-creating code workspace."); - - try { - IO.writeAll(ff, createCodeWorkspaceConfig(false)); - } catch (IOException e1) { - IrisLogging.reportError(e1); - e1.printStackTrace(); - } - if (!doOpenVSCode(f)) { - IrisLogging.warn("Tried creating code workspace but failed a second time. Your project is likely corrupt."); - } - } - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - }); - } - - private boolean doOpenVSCode(File f) throws IOException { - boolean foundWork = false; - for (File i : Objects.requireNonNull(f.listFiles())) { - if (i.getName().endsWith(".code-workspace")) { - foundWork = true; - - if (IrisSettings.get().getStudio().isOpenVSCode()) { - if (!GraphicsEnvironment.isHeadless()) { - IrisLogging.msg("Opening VSCode. You may see the output from VSCode."); - IrisLogging.msg("VSCode output always starts with: '(node:#####) electron'"); - Thread launcherThread = new Thread(() -> { - try { - Desktop.getDesktop().open(i); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - }, "Iris-VSCode-Launcher"); - launcherThread.setDaemon(true); - launcherThread.start(); - } - } - - break; - } - } - return foundWork; - } - public CompletableFuture open(VolmitSender sender, long seed, Consumer onDone) throws IrisException { if (isOpen()) { return close().thenCompose(ignored -> openInternal(sender, seed, onDone)); @@ -287,7 +106,7 @@ public class IrisProject { onDone ) ); - startStudioOpenReporter(sender, stage, progress, complete, failed); + StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed); future.whenComplete((result, throwable) -> { World maintenanceWorld = null; boolean maintenanceActive = false; @@ -322,208 +141,6 @@ public class IrisProject { return future; } - private static final int STUDIO_PROGRESS_BAR_WIDTH = 44; - - private void startStudioOpenReporter(VolmitSender sender, AtomicReference stage, AtomicReference progress, AtomicBoolean complete, AtomicBoolean failed) { - AtomicLong nextConsoleUpdate = new AtomicLong(0L); - AtomicLong startMs = new AtomicLong(System.currentTimeMillis()); - AtomicInteger taskId = new AtomicInteger(-1); - org.bukkit.boss.BossBar bossBar; - HudSlotClaim loaderClaim; - - if (sender.isPlayer() && sender.player() != null) { - bossBar = Bukkit.createBossBar( - IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING), - org.bukkit.boss.BarColor.BLUE, - org.bukkit.boss.BarStyle.SEGMENTED_20 - ); - bossBar.setProgress(0.0D); - bossBar.addPlayer(sender.player()); - bossBar.setVisible(true); - loaderClaim = BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest( - "iris:studio-open", - HudPriority.PROGRESS, - 1200L, - List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR) - )); - } else { - bossBar = null; - loaderClaim = null; - } - - int scheduledTaskId = J.ar(() -> { - double currentProgress = Math.max(0D, Math.min(0.99D, progress.get())); - String currentStage = describeStage(stage.get()); - int percent = (int) Math.round(currentProgress * 100.0D); - long elapsed = System.currentTimeMillis() - startMs.get(); - - if (complete.get()) { - J.car(taskId.get()); - - if (failed.get()) { - if (bossBar != null) { - bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); - bossBar.setColor(org.bukkit.boss.BarColor.RED); - bossBar.setTitle(IrisLanguage.text( - RuntimeProgressMessages.STUDIO_FAILED_PROGRESS, - MessageArgument.trusted("percent", percent) - )); - J.a(() -> { - bossBar.removeAll(); - bossBar.setVisible(false); - loaderClaim.release(); - BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); - }, 60); - } - if (sender.isPlayer()) { - HudSurface loaderSurface = loaderClaim.resolve(); - if (loaderSurface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); - sender.sendAction(IrisLanguage.text( - RuntimeProgressMessages.STUDIO_ACTION_FAILED, - MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), - MessageArgument.trusted("stage", currentStage) - )); - } else if (loaderSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text( - RuntimeProgressMessages.STUDIO_ACTION_FAILED, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("stage", currentStage) - ), currentProgress, BarColor.RED, BarStyle.SOLID, 4000L); - } - } else { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2)); - } - } else { - if (bossBar != null) { - bossBar.setProgress(1.0D); - bossBar.setColor(org.bukkit.boss.BarColor.GREEN); - bossBar.setTitle(IrisLanguage.text(RuntimeProgressMessages.STUDIO_READY_PROGRESS)); - J.a(() -> { - bossBar.removeAll(); - bossBar.setVisible(false); - loaderClaim.release(); - BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); - }, 60); - } - if (sender.isPlayer()) { - HudSurface loaderSurface = loaderClaim.resolve(); - if (loaderSurface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); - sender.sendAction(IrisLanguage.text( - RuntimeProgressMessages.STUDIO_ACTION_READY, - MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)), - MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) - )); - } else if (loaderSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text( - RuntimeProgressMessages.STUDIO_ACTION_READY, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) - ), 1.0D, BarColor.GREEN, BarStyle.SOLID, 4000L); - } - } else { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1))))); - } - } - return; - } - - if (sender.isPlayer() && sender.player() != null) { - if (bossBar != null) { - bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); - bossBar.setTitle(IrisLanguage.text( - RuntimeProgressMessages.STUDIO_OPENING_PROGRESS, - MessageArgument.trusted("percent", percent) - )); - } - - HudSurface loaderSurface = loaderClaim.resolve(); - if (loaderSurface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); - sender.sendAction(IrisLanguage.text( - RuntimeProgressMessages.STUDIO_ACTION_PROGRESS, - MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("stage", currentStage), - MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) - )); - } else if (loaderSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text( - RuntimeProgressMessages.STUDIO_ACTION_PROGRESS, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("stage", currentStage), - MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) - ), currentProgress, BarColor.GREEN, BarStyle.SOLID, 4000L); - } - } else { - long now = System.currentTimeMillis(); - long nextUpdate = nextConsoleUpdate.get(); - if (now >= nextUpdate) { - String bar = buildStudioConsoleBar(currentProgress); - sender.sendMessage(IrisLanguage.text( - RuntimeProgressMessages.STUDIO_CONSOLE_PROGRESS, - MessageArgument.trusted("bar", bar), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("stage", currentStage), - MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) - )); - nextConsoleUpdate.set(now + 1500L); - } - } - }, 3); - - taskId.set(scheduledTaskId); - if (complete.get()) { - J.car(taskId.get()); - } - } - - private static String buildStudioProgressBar(double progress) { - int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH); - StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4); - bar.append(C.DARK_GRAY).append("["); - for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) { - bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); - } - bar.append(C.DARK_GRAY).append("]"); - return bar.toString(); - } - - private static String buildStudioConsoleBar(double progress) { - int width = 20; - int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); - StringBuilder bar = new StringBuilder(); - bar.append("["); - for (int i = 0; i < width; i++) { - bar.append(i < filled ? "#" : "-"); - } - bar.append("]"); - return bar.toString(); - } - - private static String describeStage(String stage) { - if (stage == null || stage.isBlank()) { - return IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INITIALIZING); - } - return switch (stage) { - case "Queued" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_QUEUED); - case "resolve_dimension" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_DIMENSION); - case "prepare_world_pack" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_WORLD_PACK); - case "install_datapacks" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INSTALL_DATAPACKS); - case "create_world" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CREATE_WORLD); - case "apply_world_rules" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_APPLY_WORLD_RULES); - case "prepare_generator" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_GENERATOR); - case "request_entry_chunk" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_REQUEST_ENTRY_CHUNK); - case "resolve_safe_entry" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_SAFE_ENTRY); - case "teleport_player" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_TELEPORT_PLAYER); - case "finalize_open" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_FINALIZE_OPEN); - case "cleanup" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CLEANUP); - default -> Form.capitalizeWords(stage.replace('_', ' ')); - }; - } - public CompletableFuture close() { if (activeProvider == null) { return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null)); @@ -531,528 +148,4 @@ public class IrisProject { return StudioOpenCoordinator.get().closeProject(this); } - - public File getCodeWorkspaceFile() { - return new File(path, getName() + ".code-workspace"); - } - - public boolean updateWorkspace() { - getPath().mkdirs(); - File ws = getCodeWorkspaceFile(); - - try { - PrecisionStopwatch p = PrecisionStopwatch.start(); - JSONObject j = createCodeWorkspaceConfig(); - IO.writeAll(ws, j.toString(4)); - p.end(); - return true; - } catch (Throwable e) { - IrisLogging.reportError(e); - IrisLogging.warn("Project invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!"); - ws.delete(); - try { - IO.writeAll(ws, createCodeWorkspaceConfig()); - } catch (IOException e1) { - IrisLogging.reportError(e1); - e1.printStackTrace(); - } - } - - return false; - } - - public JSONObject createCodeWorkspaceConfig() { - return createCodeWorkspaceConfig(true); - } - - private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) { - JSONObject ws = new JSONObject(); - JSONArray folders = new JSONArray(); - JSONObject folder = new JSONObject(); - folder.put("path", "."); - folders.put(folder); - ws.put("folders", folders); - JSONObject settings = new JSONObject(); - settings.put("workbench.colorTheme", "Monokai"); - settings.put("workbench.preferredDarkColorTheme", "Solarized Dark"); - settings.put("workbench.tips.enabled", false); - settings.put("workbench.tree.indent", 24); - settings.put("files.autoSave", "onFocusChange"); - JSONObject jc = new JSONObject(); - jc.put("editor.autoIndent", "brackets"); - jc.put("editor.acceptSuggestionOnEnter", "smart"); - jc.put("editor.cursorSmoothCaretAnimation", true); - jc.put("editor.dragAndDrop", false); - jc.put("files.trimTrailingWhitespace", true); - jc.put("diffEditor.ignoreTrimWhitespace", true); - jc.put("files.trimFinalNewlines", true); - jc.put("editor.suggest.showKeywords", false); - jc.put("editor.suggest.showSnippets", false); - jc.put("editor.suggest.showWords", false); - JSONObject st = new JSONObject(); - st.put("strings", true); - jc.put("editor.quickSuggestions", st); - jc.put("editor.suggest.insertMode", "replace"); - settings.put("[json]", jc); - settings.put("json.maxItemsComputed", 30000); - JSONArray schemas = new JSONArray(); - IrisData dm = null; - if (includeSchemas) { - dm = IrisData.get(getPath()); - for (ResourceLoader r : dm.getLoaders().v()) { - if (r.supportsSchemas()) { - schemas.put(r.buildSchema()); - } - } - - for (Class i : dm.resolveSnippets()) { - try { - String snipType = i.getDeclaredAnnotation(Snippet.class).value(); - JSONObject o = new JSONObject(); - KList fm = new KList<>(); - - for (int g = 1; g < 8; g++) { - fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json"); - } - - o.put("fileMatch", new JSONArray(fm.toArray())); - o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); - schemas.put(o); - IrisData snippetData = dm; - File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); - J.attemptAsync(() -> { - try { - IO.writeAll(a, new SchemaBuilder(i, snippetData).construct().toString(4)); - } catch (Throwable e) { - e.printStackTrace(); - } - }); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - settings.put("json.schemas", schemas); - ws.put("settings", settings); - - if (!includeSchemas) { - return ws; - } - - File schemasFile = new File(path, ".idea" + File.separator + "jsonSchemas.xml"); - Document doc = IO.read(schemasFile); - Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']"); - if (mappings == null) { - mappings = doc.getRootElement() - .addElement("component") - .addAttribute("name", "JsonSchemaMappingsProjectConfiguration"); - } - - Element state = (Element) mappings.selectSingleNode("state"); - if (state == null) state = mappings.addElement("state"); - - Element map = (Element) state.selectSingleNode("map"); - if (map == null) map = state.addElement("map"); - var schemaMap = new KMap(); - schemas.forEach(element -> { - if (!(element instanceof JSONObject obj)) - return; - - String url = obj.getString("url"); - String dir = obj.getJSONArray("fileMatch").getString(0); - schemaMap.put(url, dir.substring(1, dir.indexOf("/*"))); - }); - - map.selectNodes("entry/value/SchemaInfo/option[@name='relativePathToSchema']") - .stream() - .map(node -> node.valueOf("@value")) - .forEach(schemaMap::remove); - - var ideaSchemas = map; - schemaMap.forEach((url, dir) -> { - var genName = UUID.randomUUID().toString(); - - var info = ideaSchemas.addElement("entry") - .addAttribute("key", genName) - .addElement("value") - .addElement("SchemaInfo"); - info.addElement("option") - .addAttribute("name", "generatedName") - .addAttribute("value", genName); - info.addElement("option") - .addAttribute("name", "name") - .addAttribute("value", dir); - info.addElement("option") - .addAttribute("name", "relativePathToSchema") - .addAttribute("value", url); - - - var item = info.addElement("option") - .addAttribute("name", "patterns") - .addElement("list") - .addElement("Item"); - item.addElement("option") - .addAttribute("name", "directory") - .addAttribute("value", "true"); - item.addElement("option") - .addAttribute("name", "path") - .addAttribute("value", dir); - item.addElement("option") - .addAttribute("name", "mappingKind") - .addAttribute("value", "Directory"); - }); - if (!schemaMap.isEmpty()) { - IO.write(schemasFile, doc); - } - return ws; - } - - public File compilePackage(VolmitSender sender, boolean obfuscate, boolean minify) { - String dimm = getName(); - IrisData dm = IrisData.get(path); - IrisDimension dimension = dm.getDimensionLoader().load(dimm); - File folder = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey()); - IO.delete(folder); - if (folder.exists()) { - throw new IllegalStateException("Failed to clear structure package staging folder " + folder.getAbsolutePath()); - } - if (!folder.mkdirs() && !folder.isDirectory()) { - throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath()); - } - IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : "")); - KSet regions = new KSet<>(); - KSet biomes = new KSet<>(); - KSet entities = new KSet<>(); - KSet spawners = new KSet<>(); - KSet generators = new KSet<>(); - KSet loot = new KSet<>(); - KSet blocks = new KSet<>(); - - for (String i : dm.getBlockLoader().getPossibleKeys()) { - blocks.add(dm.getBlockLoader().load(i)); - } - - dimension.getRegions().forEach((i) -> regions.add(dm.getRegionLoader().load(i))); - dimension.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))); - regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm))); - regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)))); - regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)))); - dimension.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))); - biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm)))); - biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i)))); - biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp)))); - collectSpawnerEntityKeys(spawners).forEach((i) -> entities.add(dm.getEntityLoader().load(i))); - Set structureKeys = new LinkedHashSet<>(); - collectStructureKeys(structureKeys, dimension.getStructures()); - regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures())); - biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures())); - KMap renameObjects = new KMap<>(); - String a; - StringBuilder b = new StringBuilder(); - StringBuilder c = new StringBuilder(); - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_SERIALIZING_OBJECTS)); - - for (IrisBiome i : biomes) { - for (IrisObjectPlacement j : i.getObjects()) { - b.append(j.hashCode()); - KList newNames = new KList<>(); - - for (String k : j.getPlace()) { - if (renameObjects.containsKey(k)) { - newNames.add(renameObjects.get(k)); - continue; - } - - String name = !obfuscate ? k : UUID.randomUUID().toString().replaceAll("-", ""); - b.append(name); - newNames.add(name); - renameObjects.put(k, name); - } - - j.setPlace(newNames); - } - } - - KMap> lookupObjects = renameObjects.flip(); - StringBuilder gb = new StringBuilder(); - ChronoLatch cl = new ChronoLatch(1000); - O ggg = new O<>(); - ggg.set(0); - biomes.forEach((i) -> i.getObjects().forEach((j) -> j.getPlace().forEach((k) -> - { - try { - File f = dm.getObjectLoader().findFile(lookupObjects.get(k).get(0)); - IO.copyFile(f, new File(folder, "objects/" + k + ".iob")); - gb.append(IO.hash(f)); - ggg.set(ggg.get() + 1); - - if (cl.flip()) { - int g = ggg.get(); - ggg.set(0); - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_WROTE_ANOTHER_OBJECTS, MessageArgument.untrusted("g", String.valueOf(g)))); - } - } catch (Throwable e) { - IrisLogging.reportError(e); - } - }))); - - b.append(IO.hash(gb.toString())); - c.append(IO.hash(b.toString())); - b = new StringBuilder(); - - IrisLogging.info("Writing Dimensional Scaffold"); - - try { - StructurePackageClosure structureClosure = StructurePackageClosure.collect(path, structureKeys); - if (!structureClosure.isValid()) { - throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors())); - } - b.append(structureClosure.writeTo(folder, minify)); - a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - - for (IrisGenerator i : generators) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "generators/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - c.append(IO.hash(b.toString())); - b = new StringBuilder(); - - for (IrisRegion i : regions) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "regions/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisBlockData i : blocks) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "blocks/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisBiome i : biomes) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "biomes/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisEntity i : entities) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "entities/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - for (IrisLootTable i : loot) { - a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4); - IO.writeAll(new File(folder, "loot/" + i.getLoadKey() + ".json"), a); - b.append(IO.hash(a)); - } - - c.append(IO.hash(b.toString())); - String finalHash = IO.hash(c.toString()); - JSONObject meta = new JSONObject(); - meta.put("hash", finalHash); - meta.put("time", M.ms()); - meta.put("version", dimension.getVersion()); - IO.writeAll(new File(folder, "package.json"), meta.toString(minify ? 0 : 4)); - File p = new File(IrisPlatforms.get().dataFolder(), "exports/" + dimension.getLoadKey() + ".iris"); - IrisLogging.info("Compressing Package"); - ZipUtil.pack(folder, p, 9); - IO.delete(folder); - - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_PACKAGE_COMPILED)); - return p; - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_FAILED)); - return null; - } - - static KSet collectSpawnerEntityKeys(KSet spawners) { - KSet entityKeys = new KSet<>(); - for (IrisSpawner spawner : spawners) { - spawner.getSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity())); - spawner.getInitialSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity())); - } - return entityKeys; - } - - private static void collectStructureKeys(Set keys, KList placements) { - if (placements == null) { - return; - } - for (IrisStructurePlacement placement : placements) { - if (placement == null || placement.getStructures() == null) { - continue; - } - for (String structureKey : placement.getStructures()) { - keys.add(structureKey); - } - } - } - - public void compile(VolmitSender sender) { - IrisData data = IrisData.get(getPath()); - KList jobs = new KList<>(); - KList files = new KList<>(); - KList objects = new KList<>(); - files(getPath(), files); - filesObjects(getPath(), objects); - - jobs.add(new ParallelQueueJob() { - @Override - public void execute(File f) { - try { - IrisObject o = new IrisObject(0, 0, 0); - o.read(f); - - if (o.getBlocks().isEmpty()) { - sender.sendComponent(Component.text(IrisLanguage.plain( - RuntimeUiMessages.COMPILE_IOB_EMPTY, - MessageArgument.untrusted("file", f.getName()) - ), NamedTextColor.RED) - .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain( - RuntimeUiMessages.COMPILE_IOB_EMPTY_HOVER, - MessageArgument.untrusted("path", f.getPath()) - ), NamedTextColor.YELLOW)))); - } - - if (o.getW() == 0 || o.getH() == 0 || o.getD() == 0) { - sender.sendComponent(Component.text(IrisLanguage.plain( - RuntimeUiMessages.COMPILE_IOB_NOT_3D, - MessageArgument.untrusted("file", f.getName()) - ), NamedTextColor.RED) - .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain( - RuntimeUiMessages.COMPILE_IOB_NOT_3D_HOVER, - MessageArgument.untrusted("path", f.getPath()) - ), NamedTextColor.YELLOW)))); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public String getName() { - return "IOB"; - } - }.queue(objects)); - - jobs.add(new ParallelQueueJob() { - @Override - public void execute(File f) { - try { - JSONObject p = new JSONObject(IO.readAll(f)); - fixBlocks(p); - scanForErrors(data, f, p, sender); - IO.writeAll(f, p.toString(4)); - - } catch (Throwable e) { - sender.sendComponent(Component.text(IrisLanguage.plain( - RuntimeUiMessages.COMPILE_JSON_ERROR, - MessageArgument.untrusted("file", f.getName()) - ), NamedTextColor.RED) - .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain( - RuntimeUiMessages.COMPILE_JSON_ERROR_HOVER, - MessageArgument.untrusted("path", f.getPath()), - MessageArgument.untrusted("error", String.valueOf(e.getMessage())) - ), NamedTextColor.YELLOW)))); - } - } - - @Override - public String getName() { - return "JSON"; - } - }.queue(files)); - - new JobCollection(IrisLanguage.text(RuntimeUiMessages.JOB_COMPILE), jobs).execute(sender); - } - - private void scanForErrors(IrisData data, File f, JSONObject p, VolmitSender sender) { - String key = data.toLoadKey(f); - ResourceLoader loader = data.getTypedLoaderFor(f); - - if (loader == null) { - sender.sendMessage(IrisLanguage.text( - RuntimeUiMessages.COMPILE_LOADER_NOT_FOUND, - MessageArgument.untrusted("path", f.getPath()) - )); - return; - } - - IrisRegistrant load = loader.load(key); - compare(load.getClass(), p, sender, new KList<>()); - load.scanForErrors(p, sender); - } - - public void compare(Class c, JSONObject j, VolmitSender sender, KList path) { - try { - Object o = c.getClass().getConstructor().newInstance(); - } catch (Throwable e) { - - } - } - - public void files(File clean, KList files) { - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - files(i, files); - } - } else if (clean.getName().endsWith(".json")) { - try { - files.add(clean); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } - } - - public void filesObjects(File clean, KList files) { - if (clean.isDirectory()) { - for (File i : clean.listFiles()) { - filesObjects(i, files); - } - } else if (clean.getName().endsWith(".iob")) { - try { - files.add(clean); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } - } - - private void fixBlocks(JSONObject obj) { - for (String i : obj.keySet()) { - Object o = obj.get(i); - - if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { - obj.put(i, "minecraft:" + o); - } - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o); - } - } - } - - private void fixBlocks(JSONArray obj) { - for (int i = 0; i < obj.length(); i++) { - Object o = obj.get(i); - - if (o instanceof JSONObject) { - fixBlocks((JSONObject) o); - } else if (o instanceof JSONArray) { - fixBlocks((JSONArray) o); - } - } - } } diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProjectCleaner.java b/core/src/main/java/art/arcane/iris/core/project/IrisProjectCleaner.java new file mode 100644 index 000000000..9f15dc2b3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProjectCleaner.java @@ -0,0 +1,147 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.io.IOException; + +@SuppressWarnings("ALL") +public final class IrisProjectCleaner { + private IrisProjectCleaner() { + } + + public static int clean(VolmitSender s, File clean) { + int c = 0; + if (clean.isDirectory()) { + for (File i : clean.listFiles()) { + c += clean(s, i); + } + } else if (clean.getName().endsWith(".json")) { + try { + clean(clean); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!"); + } + + c++; + } + + return c; + } + + public static void clean(File clean) throws IOException { + JSONObject obj = new JSONObject(IO.readAll(clean)); + fixBlocks(obj, clean); + + IO.writeAll(clean, obj.toString(4)); + } + + public static void fixBlocks(JSONObject obj, File f) { + for (String i : obj.keySet()) { + Object o = obj.get(i); + + if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { + obj.put(i, "minecraft:" + o); + IrisLogging.debug("Updated Block Key: " + o + " to " + obj.getString(i) + " in " + f.getPath()); + } + + if (o instanceof JSONObject) { + fixBlocks((JSONObject) o, f); + } else if (o instanceof JSONArray) { + fixBlocks((JSONArray) o, f); + } + } + } + + public static void fixBlocks(JSONArray obj, File f) { + for (int i = 0; i < obj.length(); i++) { + Object o = obj.get(i); + + if (o instanceof JSONObject) { + fixBlocks((JSONObject) o, f); + } else if (o instanceof JSONArray) { + fixBlocks((JSONArray) o, f); + } + } + } + + static void fixBlocks(JSONObject obj) { + for (String i : obj.keySet()) { + Object o = obj.get(i); + + if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) { + obj.put(i, "minecraft:" + o); + } + + if (o instanceof JSONObject) { + fixBlocks((JSONObject) o); + } else if (o instanceof JSONArray) { + fixBlocks((JSONArray) o); + } + } + } + + static void fixBlocks(JSONArray obj) { + for (int i = 0; i < obj.length(); i++) { + Object o = obj.get(i); + + if (o instanceof JSONObject) { + fixBlocks((JSONObject) o); + } else if (o instanceof JSONArray) { + fixBlocks((JSONArray) o); + } + } + } + + public static void files(File clean, KList files) { + if (clean.isDirectory()) { + for (File i : clean.listFiles()) { + files(i, files); + } + } else if (clean.getName().endsWith(".json")) { + try { + files.add(clean); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + public static void filesObjects(File clean, KList files) { + if (clean.isDirectory()) { + for (File i : clean.listFiles()) { + filesObjects(i, files); + } + } else if (clean.getName().endsWith(".iob")) { + try { + files.add(clean); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProjectCompiler.java b/core/src/main/java/art/arcane/iris/core/project/IrisProjectCompiler.java new file mode 100644 index 000000000..bfe04823c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProjectCompiler.java @@ -0,0 +1,152 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.jobs.Job; +import art.arcane.iris.util.common.scheduling.jobs.JobCollection; +import art.arcane.iris.util.common.scheduling.jobs.ParallelQueueJob; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.localization.MessageArgument; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.File; +import java.io.IOException; + +@SuppressWarnings("ALL") +public class IrisProjectCompiler { + private final IrisProject project; + + public IrisProjectCompiler(IrisProject project) { + this.project = project; + } + + public void compile(VolmitSender sender) { + IrisData data = IrisData.get(project.getPath()); + KList jobs = new KList<>(); + KList files = new KList<>(); + KList objects = new KList<>(); + IrisProjectCleaner.files(project.getPath(), files); + IrisProjectCleaner.filesObjects(project.getPath(), objects); + + jobs.add(new ParallelQueueJob() { + @Override + public void execute(File f) { + try { + IrisObject o = new IrisObject(0, 0, 0); + o.read(f); + + if (o.getBlocks().isEmpty()) { + sender.sendComponent(Component.text(IrisLanguage.plain( + RuntimeUiMessages.COMPILE_IOB_EMPTY, + MessageArgument.untrusted("file", f.getName()) + ), NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain( + RuntimeUiMessages.COMPILE_IOB_EMPTY_HOVER, + MessageArgument.untrusted("path", f.getPath()) + ), NamedTextColor.YELLOW)))); + } + + if (o.getW() == 0 || o.getH() == 0 || o.getD() == 0) { + sender.sendComponent(Component.text(IrisLanguage.plain( + RuntimeUiMessages.COMPILE_IOB_NOT_3D, + MessageArgument.untrusted("file", f.getName()) + ), NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain( + RuntimeUiMessages.COMPILE_IOB_NOT_3D_HOVER, + MessageArgument.untrusted("path", f.getPath()) + ), NamedTextColor.YELLOW)))); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public String getName() { + return "IOB"; + } + }.queue(objects)); + + jobs.add(new ParallelQueueJob() { + @Override + public void execute(File f) { + try { + JSONObject p = new JSONObject(IO.readAll(f)); + IrisProjectCleaner.fixBlocks(p); + scanForErrors(data, f, p, sender); + IO.writeAll(f, p.toString(4)); + + } catch (Throwable e) { + sender.sendComponent(Component.text(IrisLanguage.plain( + RuntimeUiMessages.COMPILE_JSON_ERROR, + MessageArgument.untrusted("file", f.getName()) + ), NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(Component.text(IrisLanguage.plain( + RuntimeUiMessages.COMPILE_JSON_ERROR_HOVER, + MessageArgument.untrusted("path", f.getPath()), + MessageArgument.untrusted("error", String.valueOf(e.getMessage())) + ), NamedTextColor.YELLOW)))); + } + } + + @Override + public String getName() { + return "JSON"; + } + }.queue(files)); + + new JobCollection(IrisLanguage.text(RuntimeUiMessages.JOB_COMPILE), jobs).execute(sender); + } + + private void scanForErrors(IrisData data, File f, JSONObject p, VolmitSender sender) { + String key = data.toLoadKey(f); + ResourceLoader loader = data.getTypedLoaderFor(f); + + if (loader == null) { + sender.sendMessage(IrisLanguage.text( + RuntimeUiMessages.COMPILE_LOADER_NOT_FOUND, + MessageArgument.untrusted("path", f.getPath()) + )); + return; + } + + IrisRegistrant load = loader.load(key); + compare(load.getClass(), p, sender, new KList<>()); + load.scanForErrors(p, sender); + } + + public void compare(Class c, JSONObject j, VolmitSender sender, KList path) { + try { + Object o = c.getClass().getConstructor().newInstance(); + } catch (Throwable e) { + + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java b/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java new file mode 100644 index 000000000..f54448336 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java @@ -0,0 +1,250 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.project; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.hud.HudPriority; +import art.arcane.volmlib.util.hud.HudSlotClaim; +import art.arcane.volmlib.util.hud.HudSlotRequest; +import art.arcane.volmlib.util.hud.HudSurface; +import art.arcane.volmlib.util.localization.MessageArgument; +import org.bukkit.Bukkit; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +@SuppressWarnings("ALL") +final class StudioOpenProgressReporter { + private static final int STUDIO_PROGRESS_BAR_WIDTH = 44; + + private StudioOpenProgressReporter() { + } + + static void startStudioOpenReporter(VolmitSender sender, AtomicReference stage, AtomicReference progress, AtomicBoolean complete, AtomicBoolean failed) { + AtomicLong nextConsoleUpdate = new AtomicLong(0L); + AtomicLong startMs = new AtomicLong(System.currentTimeMillis()); + AtomicInteger taskId = new AtomicInteger(-1); + org.bukkit.boss.BossBar bossBar; + HudSlotClaim loaderClaim; + + if (sender.isPlayer() && sender.player() != null) { + bossBar = Bukkit.createBossBar( + IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING), + org.bukkit.boss.BarColor.BLUE, + org.bukkit.boss.BarStyle.SEGMENTED_20 + ); + bossBar.setProgress(0.0D); + bossBar.addPlayer(sender.player()); + bossBar.setVisible(true); + loaderClaim = BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest( + "iris:studio-open", + HudPriority.PROGRESS, + 1200L, + List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR) + )); + } else { + bossBar = null; + loaderClaim = null; + } + + int scheduledTaskId = J.ar(() -> { + double currentProgress = Math.max(0D, Math.min(0.99D, progress.get())); + String currentStage = describeStage(stage.get()); + int percent = (int) Math.round(currentProgress * 100.0D); + long elapsed = System.currentTimeMillis() - startMs.get(); + + if (complete.get()) { + J.car(taskId.get()); + + if (failed.get()) { + if (bossBar != null) { + bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); + bossBar.setColor(org.bukkit.boss.BarColor.RED); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_FAILED_PROGRESS, + MessageArgument.trusted("percent", percent) + )); + J.a(() -> { + bossBar.removeAll(); + bossBar.setVisible(false); + loaderClaim.release(); + BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); + }, 60); + } + if (sender.isPlayer()) { + HudSurface loaderSurface = loaderClaim.resolve(); + if (loaderSurface == HudSurface.ACTION_BAR) { + BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_FAILED, + MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), + MessageArgument.trusted("stage", currentStage) + )); + } else if (loaderSurface == HudSurface.BOSS_BAR) { + BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_FAILED, + MessageArgument.trusted("bar", ""), + MessageArgument.trusted("stage", currentStage) + ), currentProgress, BarColor.RED, BarStyle.SOLID, 4000L); + } + } else { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2)); + } + } else { + if (bossBar != null) { + bossBar.setProgress(1.0D); + bossBar.setColor(org.bukkit.boss.BarColor.GREEN); + bossBar.setTitle(IrisLanguage.text(RuntimeProgressMessages.STUDIO_READY_PROGRESS)); + J.a(() -> { + bossBar.removeAll(); + bossBar.setVisible(false); + loaderClaim.release(); + BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); + }, 60); + } + if (sender.isPlayer()) { + HudSurface loaderSurface = loaderClaim.resolve(); + if (loaderSurface == HudSurface.ACTION_BAR) { + BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_READY, + MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); + } else if (loaderSurface == HudSurface.BOSS_BAR) { + BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_READY, + MessageArgument.trusted("bar", ""), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + ), 1.0D, BarColor.GREEN, BarStyle.SOLID, 4000L); + } + } else { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1))))); + } + } + return; + } + + if (sender.isPlayer() && sender.player() != null) { + if (bossBar != null) { + bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_OPENING_PROGRESS, + MessageArgument.trusted("percent", percent) + )); + } + + HudSurface loaderSurface = loaderClaim.resolve(); + if (loaderSurface == HudSurface.ACTION_BAR) { + BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open"); + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_PROGRESS, + MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + } else if (loaderSurface == HudSurface.BOSS_BAR) { + BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_PROGRESS, + MessageArgument.trusted("bar", ""), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + ), currentProgress, BarColor.GREEN, BarStyle.SOLID, 4000L); + } + } else { + long now = System.currentTimeMillis(); + long nextUpdate = nextConsoleUpdate.get(); + if (now >= nextUpdate) { + String bar = buildStudioConsoleBar(currentProgress); + sender.sendMessage(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_CONSOLE_PROGRESS, + MessageArgument.trusted("bar", bar), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + nextConsoleUpdate.set(now + 1500L); + } + } + }, 3); + + taskId.set(scheduledTaskId); + if (complete.get()) { + J.car(taskId.get()); + } + } + + private static String buildStudioProgressBar(double progress) { + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH); + StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) { + bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + return bar.toString(); + } + + private static String buildStudioConsoleBar(double progress) { + int width = 20; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); + StringBuilder bar = new StringBuilder(); + bar.append("["); + for (int i = 0; i < width; i++) { + bar.append(i < filled ? "#" : "-"); + } + bar.append("]"); + return bar.toString(); + } + + private static String describeStage(String stage) { + if (stage == null || stage.isBlank()) { + return IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INITIALIZING); + } + return switch (stage) { + case "Queued" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_QUEUED); + case "resolve_dimension" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_DIMENSION); + case "prepare_world_pack" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_WORLD_PACK); + case "install_datapacks" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_INSTALL_DATAPACKS); + case "create_world" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CREATE_WORLD); + case "apply_world_rules" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_APPLY_WORLD_RULES); + case "prepare_generator" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_PREPARE_GENERATOR); + case "request_entry_chunk" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_REQUEST_ENTRY_CHUNK); + case "resolve_safe_entry" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_RESOLVE_SAFE_ENTRY); + case "teleport_player" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_TELEPORT_PLAYER); + case "finalize_open" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_FINALIZE_OPEN); + case "cleanup" -> IrisLanguage.text(RuntimeProgressMessages.STUDIO_STAGE_CLEANUP); + default -> Form.capitalizeWords(stage.replace('_', ' ')); + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java index 1934b2960..c77995e88 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -6,6 +6,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisCodeWorkspace; import art.arcane.iris.core.tools.IrisCreator; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.platform.PlatformChunkGenerator; @@ -185,7 +186,7 @@ public final class StudioOpenCoordinator { request.project().setActiveProvider(provider); } if (request.openWorkspace() && request.project() != null) { - request.project().openVSCode(request.sender()); + new IrisCodeWorkspace(request.project()).openVSCode(request.sender()); } if (request.onDone() != null) { request.onDone().accept(world); diff --git a/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java b/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java index a283a79f8..352140b9c 100644 --- a/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/ExternalDataSVC.java @@ -40,7 +40,6 @@ import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.MissingResourceException; @@ -112,7 +111,13 @@ public class ExternalDataSVC implements IrisService { } public Optional getBlockData(final Identifier key) { - Pair> pair = parseState(key); + Pair> pair; + try { + pair = parseState(key); + } catch (IllegalArgumentException e) { + IrisLogging.error(e.getMessage()); + return Optional.empty(); + } Identifier mod = pair.getA(); Optional provider = activeProviders.stream().filter(p -> p.isValidProvider(mod, DataType.BLOCK)).findFirst(); @@ -190,15 +195,25 @@ public class ExternalDataSVC implements IrisService { } public static Pair> parseState(Identifier key) { - if (!key.key().contains("[") || !key.key().contains("]")) { + String raw = key.key(); + int open = raw.indexOf('['); + int close = raw.lastIndexOf(']'); + if (open < 0 || close < open) { return new Pair<>(key, new KMap<>()); } - String state = key.key().split("\\Q[\\E")[1].split("\\Q]\\E")[0]; + + String state = raw.substring(open + 1, close); KMap stateMap = new KMap<>(); if (!state.isEmpty()) { - Arrays.stream(state.split(",")).forEach(s -> stateMap.put(s.split("=")[0], s.split("=")[1])); + for (String entry : state.split(",")) { + String[] pair = entry.split("=", 2); + if (pair.length != 2 || pair[0].isBlank() || pair[1].isBlank()) { + throw new IllegalArgumentException("Malformed block state \"" + entry + "\" in \"" + key + "\" (expected key=value)"); + } + stateMap.put(pair[0], pair[1]); + } } - return new Pair<>(new Identifier(key.namespace(), key.key().split("\\Q[\\E")[0]), stateMap); + return new Pair<>(new Identifier(key.namespace(), raw.substring(0, open)), stateMap); } public static Identifier buildState(Identifier key, KMap state) { diff --git a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java index d7bc1b041..9bfd0e140 100644 --- a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java @@ -1,5 +1,6 @@ package art.arcane.iris.core.service; +import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorldStorage; import art.arcane.iris.core.pregenerator.cache.PregenCache; @@ -24,10 +25,11 @@ import java.lang.ref.WeakReference; import java.util.function.Function; public class GlobalCacheSVC implements IrisService { + private static final long TRIMMER_JOIN_MS = 2_000; private static final KMap> REFERENCE_CACHE = new KMap<>(); private final KMap globalCache = new KMap<>(); private transient boolean lastState; - private static boolean disabled = true; + private static volatile boolean disabled = true; private Looper trimmer; @Override @@ -55,11 +57,16 @@ public class GlobalCacheSVC implements IrisService { public void onDisable() { disabled = true; Looper activeTrimmer = trimmer; + trimmer = null; if (activeTrimmer != null) { + activeTrimmer.interrupt(); try { - activeTrimmer.join(); + activeTrimmer.join(TRIMMER_JOIN_MS); } catch (InterruptedException ignored) { } + if (activeTrimmer.isAlive()) { + IrisLogging.warn("Global cache trimmer did not stop within " + TRIMMER_JOIN_MS + "ms."); + } } globalCache.qclear((world, cache) -> cache.write()); } diff --git a/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java b/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java index 53c7f3b55..52ec73137 100644 --- a/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/PreservationSVC.java @@ -86,6 +86,10 @@ public class PreservationSVC implements IrisService, PreservationRegistry { return 60000; } }; + dereferencer.setName("Iris Preservation"); + dereferencer.setDaemon(true); + dereferencer.setPriority(Thread.MIN_PRIORITY); + dereferencer.start(); } @Override diff --git a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java index 62321f454..0ae07e4f6 100644 --- a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java @@ -31,6 +31,8 @@ import art.arcane.iris.core.pack.PackDownloader; import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.core.project.IrisPackageCompiler; +import art.arcane.iris.core.project.IrisCodeWorkspace; import art.arcane.iris.core.project.IrisProjectCopier; import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; import art.arcane.iris.core.tools.IrisToolbelt; @@ -378,7 +380,7 @@ public class StudioSVC implements IrisService { } public void openVSCode(VolmitSender sender, String dim) { - new IrisProject(new File(getWorkspaceFolder(), dim)).openVSCode(sender); + new IrisCodeWorkspace(new IrisProject(new File(getWorkspaceFolder(), dim))).openVSCode(sender); } public File getWorkspaceFolder(String... sub) { @@ -475,7 +477,7 @@ public class StudioSVC implements IrisService { } public File compilePackage(VolmitSender sender, String d, boolean obfuscate, boolean minify) { - return new IrisProject(new File(getWorkspaceFolder(), d)).compilePackage(sender, obfuscate, minify); + return new IrisPackageCompiler(new IrisProject(new File(getWorkspaceFolder(), d))).compilePackage(sender, obfuscate, minify); } public void createFrom(String existingPack, String newName) { @@ -496,7 +498,7 @@ public class StudioSVC implements IrisService { try { IrisProject p = new IrisProject(getWorkspaceFolder(newName)); - JSONObject ws = p.createCodeWorkspaceConfig(); + JSONObject ws = new IrisCodeWorkspace(p).createCodeWorkspaceConfig(); IO.writeAll(getWorkspaceFile(newName, newName + ".code-workspace"), ws.toString(0)); } catch (JSONException | IOException e) { IrisLogging.reportError(e); @@ -548,7 +550,7 @@ public class StudioSVC implements IrisService { public void updateWorkspace() { if (isProjectOpen()) { - activeProject.updateWorkspace(); + new IrisCodeWorkspace(activeProject).updateWorkspace(); } } } diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCell.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCell.java deleted file mode 100644 index ac61cb3dd..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCell.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Pattern; - -public record SimpleStructureStudioCell( - int x, - int z, - SimpleStructureStudioTopology topology, - int quarterTurns, - SimpleStructureStudioRotationPolicy rotationPolicy, - String connectorChannel, - int connectorHeight, - List variants, - int activeVariantIndex -) { - public static final String DEFAULT_CONNECTOR_CHANNEL = "path"; - - private static final Pattern CONNECTOR_CHANNEL_PATTERN = Pattern.compile( - "(?:[a-z0-9._-]+:)?[a-z0-9._-]+(?:/[a-z0-9._-]+)*" - ); - - public SimpleStructureStudioCell { - if (x < 0 || z < 0) { - throw new IllegalArgumentException("Cell coordinates cannot be negative: " + x + ", " + z); - } - Objects.requireNonNull(topology, "topology"); - Objects.requireNonNull(rotationPolicy, "rotationPolicy"); - quarterTurns = Math.floorMod(quarterTurns, 4); - if (!rotationPolicy.allows(quarterTurns)) { - throw new IllegalArgumentException( - "Rotation policy " + rotationPolicy + " does not allow quarter turn " + quarterTurns - ); - } - Objects.requireNonNull(connectorChannel, "connectorChannel"); - if (!CONNECTOR_CHANNEL_PATTERN.matcher(connectorChannel).matches()) { - throw new IllegalArgumentException("Invalid connector channel: " + connectorChannel); - } - if (connectorHeight < 0) { - throw new IllegalArgumentException("Connector height cannot be negative: " + connectorHeight); - } - Objects.requireNonNull(variants, "variants"); - variants = List.copyOf(variants); - validateVariants(variants); - if (topology == SimpleStructureStudioTopology.EMPTY && !variants.isEmpty()) { - throw new IllegalArgumentException("Empty cells cannot contain variants"); - } - if (variants.isEmpty() && activeVariantIndex != -1) { - throw new IllegalArgumentException("A cell without variants must use activeVariantIndex -1"); - } - if (!variants.isEmpty() && (activeVariantIndex < 0 || activeVariantIndex >= variants.size())) { - throw new IllegalArgumentException("Active variant index is outside the variant list: " + activeVariantIndex); - } - } - - public static SimpleStructureStudioCell empty(int x, int z) { - return new SimpleStructureStudioCell( - x, - z, - SimpleStructureStudioTopology.EMPTY, - 0, - SimpleStructureStudioRotationPolicy.FIXED, - DEFAULT_CONNECTOR_CHANNEL, - 0, - List.of(), - -1 - ); - } - - public static SimpleStructureStudioCell create(int x, int z, SimpleStructureStudioTopology topology) { - Objects.requireNonNull(topology, "topology"); - if (topology == SimpleStructureStudioTopology.EMPTY) { - return empty(x, z); - } - return new SimpleStructureStudioCell( - x, - z, - topology, - 0, - SimpleStructureStudioRotationPolicy.QUARTER_TURNS, - DEFAULT_CONNECTOR_CHANNEL, - 0, - List.of(), - -1 - ); - } - - public boolean isEmpty() { - return topology == SimpleStructureStudioTopology.EMPTY; - } - - public int connectorMask() { - return topology.connectorMask(quarterTurns); - } - - public boolean connects(SimpleStructureStudioDirection direction) { - return topology.connects(direction, quarterTurns); - } - - public Optional activeVariant() { - if (activeVariantIndex < 0) { - return Optional.empty(); - } - return Optional.of(variants.get(activeVariantIndex)); - } - - public SimpleStructureStudioCell withTopology(SimpleStructureStudioTopology newTopology) { - Objects.requireNonNull(newTopology, "newTopology"); - if (newTopology == SimpleStructureStudioTopology.EMPTY) { - return empty(x, z); - } - if (isEmpty()) { - return create(x, z, newTopology); - } - return new SimpleStructureStudioCell( - x, - z, - newTopology, - quarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - variants, - activeVariantIndex - ); - } - - public SimpleStructureStudioCell withQuarterTurns(int newQuarterTurns) { - return new SimpleStructureStudioCell( - x, - z, - topology, - newQuarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - variants, - activeVariantIndex - ); - } - - public SimpleStructureStudioCell rotateClockwise() { - return withQuarterTurns(rotationPolicy.next(quarterTurns)); - } - - public SimpleStructureStudioCell rotateCounterClockwise() { - return withQuarterTurns(rotationPolicy.previous(quarterTurns)); - } - - public SimpleStructureStudioCell withRotationPolicy(SimpleStructureStudioRotationPolicy newPolicy) { - Objects.requireNonNull(newPolicy, "newPolicy"); - int newQuarterTurns = newPolicy.allows(quarterTurns) ? quarterTurns : 0; - return new SimpleStructureStudioCell( - x, - z, - topology, - newQuarterTurns, - newPolicy, - connectorChannel, - connectorHeight, - variants, - activeVariantIndex - ); - } - - public SimpleStructureStudioCell withConnector(String newChannel, int newHeight) { - return new SimpleStructureStudioCell( - x, - z, - topology, - quarterTurns, - rotationPolicy, - newChannel, - newHeight, - variants, - activeVariantIndex - ); - } - - public SimpleStructureStudioCell addVariant(SimpleStructureStudioVariant variant) { - Objects.requireNonNull(variant, "variant"); - List updatedVariants = new ArrayList<>(variants); - updatedVariants.add(variant); - int newActiveIndex = activeVariantIndex < 0 ? 0 : activeVariantIndex; - return new SimpleStructureStudioCell( - x, - z, - topology, - quarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - updatedVariants, - newActiveIndex - ); - } - - public SimpleStructureStudioCell setVariantWeight(String variantId, int weight) { - int variantIndex = requireVariantIndex(variantId); - List updatedVariants = new ArrayList<>(variants); - updatedVariants.set(variantIndex, updatedVariants.get(variantIndex).withWeight(weight)); - return new SimpleStructureStudioCell( - x, - z, - topology, - quarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - updatedVariants, - activeVariantIndex - ); - } - - public SimpleStructureStudioCell removeVariant(String variantId) { - int variantIndex = requireVariantIndex(variantId); - List updatedVariants = new ArrayList<>(variants); - updatedVariants.remove(variantIndex); - int newActiveIndex = activeVariantIndex; - if (updatedVariants.isEmpty()) { - newActiveIndex = -1; - } else if (variantIndex < activeVariantIndex) { - newActiveIndex--; - } else if (newActiveIndex >= updatedVariants.size()) { - newActiveIndex = updatedVariants.size() - 1; - } - return new SimpleStructureStudioCell( - x, - z, - topology, - quarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - updatedVariants, - newActiveIndex - ); - } - - public SimpleStructureStudioCell selectVariant(String variantId) { - int variantIndex = requireVariantIndex(variantId); - return new SimpleStructureStudioCell( - x, - z, - topology, - quarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - variants, - variantIndex - ); - } - - public SimpleStructureStudioCell cycleVariant(int offset) { - if (variants.isEmpty()) { - return this; - } - int newActiveIndex = Math.floorMod((long) activeVariantIndex + offset, variants.size()); - return new SimpleStructureStudioCell( - x, - z, - topology, - quarterTurns, - rotationPolicy, - connectorChannel, - connectorHeight, - variants, - newActiveIndex - ); - } - - private static void validateVariants(List variants) { - Set variantIds = new HashSet<>(); - for (SimpleStructureStudioVariant variant : variants) { - Objects.requireNonNull(variant, "variant"); - if (!variantIds.add(variant.id())) { - throw new IllegalArgumentException("Duplicate variant id: " + variant.id()); - } - } - } - - private int requireVariantIndex(String variantId) { - Objects.requireNonNull(variantId, "variantId"); - for (int i = 0; i < variants.size(); i++) { - if (variants.get(i).id().equals(variantId)) { - return i; - } - } - throw new IllegalArgumentException("Unknown variant id: " + variantId); - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompiler.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompiler.java deleted file mode 100644 index 116a819b8..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompiler.java +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import art.arcane.iris.core.structure.authoring.StructureBackend; -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; -import art.arcane.iris.core.structure.authoring.StructureSource; -import art.arcane.iris.engine.framework.structure.StructureGraphCompilation; -import art.arcane.iris.engine.framework.structure.StructureGraphCompiler; -import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic; -import art.arcane.iris.engine.framework.structure.StructureGraphResolver; -import art.arcane.iris.engine.object.IrisDirection; -import art.arcane.iris.engine.object.IrisJigsawConnector; -import art.arcane.iris.engine.object.IrisJigsawPiece; -import art.arcane.iris.engine.object.IrisJigsawPieceEntry; -import art.arcane.iris.engine.object.IrisJigsawPool; -import art.arcane.iris.engine.object.IrisObject; -import art.arcane.iris.engine.object.IrisPosition; -import art.arcane.iris.engine.object.IrisStructure; -import art.arcane.iris.engine.object.JigsawJoint; -import art.arcane.volmlib.util.collection.KList; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; - -public final class SimpleStructureStudioCompiler { - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - private SimpleStructureStudioCompiler() { - } - - public static StructureResourceBundle compile( - SimpleStructureStudioDraft draft, - SimpleStructureStudioPublishConfig config, - Map resolvedVariants - ) throws IOException { - CompilationInput input = new CompilationInput(draft, config, resolvedVariants); - return new CompilationState(input).compile(); - } - - private static final class CompilationState { - private final SimpleStructureStudioDraft draft; - private final SimpleStructureStudioPublishConfig config; - private final Map resolvedVariants; - private final Map pools; - private final Map pieces; - private final Map objects; - private final List startEntries; - private final List mainEntries; - private final List terminalEntries; - private final String startPoolKey; - private final String mainPoolKey; - private final String terminalPoolKey; - - private CompilationState(CompilationInput input) { - draft = input.draft(); - config = input.config(); - resolvedVariants = input.resolvedVariants(); - pools = new LinkedHashMap<>(); - pieces = new LinkedHashMap<>(); - objects = new LinkedHashMap<>(); - startEntries = new ArrayList<>(); - mainEntries = new ArrayList<>(); - terminalEntries = new ArrayList<>(); - startPoolKey = config.resourceKey() + "/start"; - mainPoolKey = config.resourceKey() + "/main"; - terminalPoolKey = config.resourceKey() + "/terminal"; - } - - private StructureResourceBundle compile() throws IOException { - validateDraft(); - validateResolvedVariants(); - compilePieces(); - compilePools(); - IrisStructure structure = compileStructure(); - validateGraph(structure); - return bundle(structure); - } - - private void validateDraft() { - if (!draft.hasContent()) { - throw new IllegalStateException("A Studio structure must contain authored tiles"); - } - - TreeSet startChannels = new TreeSet<>(); - TreeSet mainChannels = new TreeSet<>(); - TreeSet terminalChannels = new TreeSet<>(); - for (SimpleStructureStudioCell cell : draft.cells()) { - if (cell.rotationPolicy() == SimpleStructureStudioRotationPolicy.HALF_TURNS) { - throw new IllegalStateException( - "HALF_TURNS cannot be represented by the Iris jigsaw rotatable contract at cell " - + cell.x() + ", " + cell.z() - ); - } - if (cell.variants().isEmpty()) { - throw new IllegalStateException( - "Studio cell " + cell.x() + ", " + cell.z() + " has no captured variants" - ); - } - channelsFor(cell.topology(), startChannels, mainChannels, terminalChannels).add( - cell.connectorChannel() - ); - } - - requireChannels("START", startChannels); - requireChannels("main", mainChannels); - requireChannels("TERMINAL", terminalChannels); - if (!startChannels.equals(mainChannels) || !startChannels.equals(terminalChannels)) { - throw new IllegalStateException( - "START, main, and TERMINAL tiles must cover the same connector channels: start=" - + startChannels + ", main=" + mainChannels + ", terminal=" + terminalChannels - ); - } - } - - private Set channelsFor( - SimpleStructureStudioTopology topology, - Set startChannels, - Set mainChannels, - Set terminalChannels - ) { - return switch (topology) { - case START -> startChannels; - case TERMINAL -> terminalChannels; - case EMPTY -> throw new IllegalStateException("Drafts cannot publish empty cells"); - default -> mainChannels; - }; - } - - private void requireChannels(String category, Set channels) { - if (channels.isEmpty()) { - throw new IllegalStateException("A Studio structure must contain at least one " + category + " tile"); - } - } - - private void validateResolvedVariants() { - LinkedHashSet expected = new LinkedHashSet<>(); - for (SimpleStructureStudioCell cell : draft.cells()) { - for (SimpleStructureStudioVariant variant : cell.variants()) { - expected.add(SimpleStructureStudioVariantKey.of(cell, variant)); - } - } - - for (Map.Entry entry : resolvedVariants.entrySet()) { - if (entry.getKey() == null || entry.getValue() == null) { - throw new IllegalArgumentException("Resolved Studio variants cannot contain null keys or objects"); - } - } - LinkedHashSet actual = new LinkedHashSet<>(resolvedVariants.keySet()); - if (!actual.equals(expected)) { - LinkedHashSet missing = new LinkedHashSet<>(expected); - missing.removeAll(actual); - LinkedHashSet unexpected = new LinkedHashSet<>(actual); - unexpected.removeAll(expected); - throw new IllegalStateException( - "Resolved Studio variants do not match the draft: missing=" + describe(missing) - + ", unexpected=" + describe(unexpected) - ); - } - } - - private List describe(Set keys) { - TreeSet descriptions = new TreeSet<>(); - for (SimpleStructureStudioVariantKey key : keys) { - descriptions.add(key.cellX() + "," + key.cellZ() + ":" + key.variantId()); - } - return List.copyOf(descriptions); - } - - private void compilePieces() { - for (SimpleStructureStudioCell cell : draft.cells()) { - for (SimpleStructureStudioVariant variant : cell.variants()) { - SimpleStructureStudioVariantKey variantKey = SimpleStructureStudioVariantKey.of(cell, variant); - IrisObject object = resolvedVariants.get(variantKey); - validateObject(variantKey, object); - String resourceKey = variantResourceKey(cell, variant); - IrisJigsawPiece piece = new IrisJigsawPiece() - .setObject(resourceKey) - .setConnectors(connectors(cell, object)) - .setRotatable(cell.rotationPolicy() == SimpleStructureStudioRotationPolicy.QUARTER_TURNS); - objects.put(resourceKey, object); - pieces.put(resourceKey, piece); - entriesFor(cell.topology()).add(new PieceEntry(resourceKey, variant.weight())); - } - } - } - - private void validateObject(SimpleStructureStudioVariantKey key, IrisObject object) { - SimpleStructureStudioLayout layout = draft.layout(); - if (object.getW() != layout.cellWidth() - || object.getH() != layout.captureHeight() - || object.getD() != layout.cellDepth()) { - throw new IllegalStateException( - "Resolved object " + key.cellX() + "," + key.cellZ() + ":" + key.variantId() - + " has dimensions " + object.getW() + "x" + object.getH() + "x" + object.getD() - + "; expected " + layout.cellWidth() + "x" + layout.captureHeight() + "x" - + layout.cellDepth() - ); - } - } - - private String variantResourceKey( - SimpleStructureStudioCell cell, - SimpleStructureStudioVariant variant - ) { - return config.resourceKey() + "/cells/" + cell.x() + "-" + cell.z() + "/" + variant.id(); - } - - private KList connectors(SimpleStructureStudioCell cell, IrisObject object) { - KList connectors = new KList<>(); - for (SimpleStructureStudioDirection direction : SimpleStructureStudioDirection.values()) { - if (!cell.connects(direction)) { - continue; - } - connectors.add(new IrisJigsawConnector() - .setPosition(connectorPosition(direction, cell.connectorHeight(), object)) - .setDirection(irisDirection(direction)) - .setPool(mainPoolKey) - .setName(cell.connectorChannel()) - .setTargetName(cell.connectorChannel()) - .setJoint(JigsawJoint.ALIGNED)); - } - return connectors; - } - - private IrisPosition connectorPosition( - SimpleStructureStudioDirection direction, - int height, - IrisObject object - ) { - return switch (direction) { - case NORTH -> new IrisPosition(object.getW() / 2, height, 0); - case EAST -> new IrisPosition(object.getW() - 1, height, object.getD() / 2); - case SOUTH -> new IrisPosition(object.getW() / 2, height, object.getD() - 1); - case WEST -> new IrisPosition(0, height, object.getD() / 2); - }; - } - - private IrisDirection irisDirection(SimpleStructureStudioDirection direction) { - return switch (direction) { - case NORTH -> IrisDirection.NORTH_NEGATIVE_Z; - case EAST -> IrisDirection.EAST_POSITIVE_X; - case SOUTH -> IrisDirection.SOUTH_POSITIVE_Z; - case WEST -> IrisDirection.WEST_NEGATIVE_X; - }; - } - - private List entriesFor(SimpleStructureStudioTopology topology) { - return switch (topology) { - case START -> startEntries; - case TERMINAL -> terminalEntries; - case EMPTY -> throw new IllegalStateException("Drafts cannot publish empty cells"); - default -> mainEntries; - }; - } - - private void compilePools() { - pools.put(startPoolKey, pool(startEntries, "")); - pools.put(mainPoolKey, pool(mainEntries, terminalPoolKey)); - pools.put(terminalPoolKey, pool(terminalEntries, "")); - } - - private IrisJigsawPool pool(List entries, String fallback) { - KList weightedPieces = new KList<>(); - for (PieceEntry entry : entries) { - weightedPieces.add(new IrisJigsawPieceEntry(entry.pieceKey(), entry.weight())); - } - return new IrisJigsawPool().setPieces(weightedPieces).setFallback(fallback); - } - - private IrisStructure compileStructure() { - IrisStructure structure = new IrisStructure() - .setStartPool(startPoolKey) - .setMaxDepth(config.maxDepth()) - .setMaxSizeChunks(config.maxSizeChunks()) - .setPlaceMode(config.placeMode()); - structure.setLoadKey(config.resourceKey()); - return structure; - } - - private void validateGraph(IrisStructure structure) { - StructureGraphCompilation compilation = StructureGraphCompiler.compile( - structure, - new BundleGraphResolver(pools, pieces, objects) - ); - if (compilation.isAssemblyViable() && compilation.getDiagnostics().isEmpty()) { - return; - } - - StringBuilder failure = new StringBuilder("Studio structure graph is not safely assemblable"); - for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) { - failure.append("; ").append(diagnostic.code()).append(": ").append(diagnostic.message()); - } - if (!compilation.isAssemblyViable() && compilation.getDiagnostics().isEmpty()) { - failure.append("; deterministic assembly samples did not complete"); - } - throw new IllegalStateException(failure.toString()); - } - - private StructureResourceBundle bundle(IrisStructure structure) throws IOException { - StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(config.structureKey()) - .source(StructureSource.of(StructureSource.Kind.IRIS, config.structureKey())) - .backend(StructureBackend.IRIS_ASSEMBLY) - .capability(StructureCapability.BLOCKS) - .capability(StructureCapability.BLOCK_ENTITIES) - .capability(StructureCapability.CONNECTORS) - .capability(StructureCapability.IRIS_PLACEMENT) - .textResource("structures/" + config.resourceKey() + ".json", GSON.toJson(structure)); - - for (Map.Entry entry : objects.entrySet()) { - bundle.resource("objects/" + entry.getKey() + ".iob", serialize(entry.getValue())); - } - for (Map.Entry entry : pieces.entrySet()) { - bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue())); - } - for (Map.Entry entry : pools.entrySet()) { - bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue())); - } - return bundle.build(); - } - - private byte[] serialize(IrisObject object) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - object.write(output); - return output.toByteArray(); - } - } - - private record CompilationInput( - SimpleStructureStudioDraft draft, - SimpleStructureStudioPublishConfig config, - Map resolvedVariants - ) { - private CompilationInput { - Objects.requireNonNull(draft, "draft"); - Objects.requireNonNull(config, "config"); - Objects.requireNonNull(resolvedVariants, "resolvedVariants"); - } - } - - private record PieceEntry(String pieceKey, int weight) { - } - - private static final class BundleGraphResolver implements StructureGraphResolver { - private final Map pools; - private final Map pieces; - private final Map objects; - - private BundleGraphResolver( - Map pools, - Map pieces, - Map objects - ) { - this.pools = pools; - this.pieces = pieces; - this.objects = objects; - } - - @Override - public IrisJigsawPool loadPool(String key) { - return pools.get(key); - } - - @Override - public IrisJigsawPiece loadPiece(String key) { - return pieces.get(key); - } - - @Override - public IrisObject loadObject(String key) { - return objects.get(key); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDirection.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDirection.java deleted file mode 100644 index 642b736d4..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDirection.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -public enum SimpleStructureStudioDirection { - NORTH(1, 0, -1), - EAST(2, 1, 0), - SOUTH(4, 0, 1), - WEST(8, -1, 0); - - public static final int ALL_MASK = 15; - - private static final SimpleStructureStudioDirection[] ORDERED = values(); - - private final int mask; - private final int offsetX; - private final int offsetZ; - - SimpleStructureStudioDirection(int mask, int offsetX, int offsetZ) { - this.mask = mask; - this.offsetX = offsetX; - this.offsetZ = offsetZ; - } - - public int mask() { - return mask; - } - - public int offsetX() { - return offsetX; - } - - public int offsetZ() { - return offsetZ; - } - - public SimpleStructureStudioDirection rotateClockwise(int quarterTurns) { - int rotatedIndex = Math.floorMod((long) ordinal() + quarterTurns, ORDERED.length); - return ORDERED[rotatedIndex]; - } - - public static int rotateMask(int connectorMask, int quarterTurns) { - if ((connectorMask & ~ALL_MASK) != 0) { - throw new IllegalArgumentException("Connector mask uses unsupported direction bits: " + connectorMask); - } - int normalizedTurns = Math.floorMod(quarterTurns, ORDERED.length); - if (normalizedTurns == 0 || connectorMask == 0) { - return connectorMask; - } - int rotatedMask = 0; - for (SimpleStructureStudioDirection direction : ORDERED) { - if ((connectorMask & direction.mask) != 0) { - rotatedMask |= direction.rotateClockwise(normalizedTurns).mask; - } - } - return rotatedMask; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDraft.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDraft.java deleted file mode 100644 index 17ecf6206..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioDraft.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -public record SimpleStructureStudioDraft( - SimpleStructureStudioLayout layout, - long previewSeed, - List cells -) { - private static final Comparator CELL_ORDER = Comparator - .comparingInt(SimpleStructureStudioCell::z) - .thenComparingInt(SimpleStructureStudioCell::x); - - public SimpleStructureStudioDraft { - Objects.requireNonNull(layout, "layout"); - Objects.requireNonNull(cells, "cells"); - List orderedCells = new ArrayList<>(cells); - orderedCells.sort(CELL_ORDER); - validateCells(layout, orderedCells); - cells = List.copyOf(orderedCells); - } - - public static SimpleStructureStudioDraft empty(SimpleStructureStudioLayout layout, long previewSeed) { - return new SimpleStructureStudioDraft(layout, previewSeed, List.of()); - } - - public boolean hasContent() { - return !cells.isEmpty(); - } - - public Optional cellAt(int x, int z) { - requirePosition(x, z); - for (SimpleStructureStudioCell cell : cells) { - if (cell.x() == x && cell.z() == z) { - return Optional.of(cell); - } - } - return Optional.empty(); - } - - public SimpleStructureStudioCell cellOrEmpty(int x, int z) { - return cellAt(x, z).orElseGet(() -> SimpleStructureStudioCell.empty(x, z)); - } - - public SimpleStructureStudioDraft withLayout(SimpleStructureStudioLayout newLayout) { - Objects.requireNonNull(newLayout, "newLayout"); - if (hasContent() && !layout.equals(newLayout)) { - throw new IllegalStateException("The studio layout cannot be resized after content has been added"); - } - return new SimpleStructureStudioDraft(newLayout, previewSeed, cells); - } - - public SimpleStructureStudioDraft withPreviewSeed(long newPreviewSeed) { - return new SimpleStructureStudioDraft(layout, newPreviewSeed, cells); - } - - public SimpleStructureStudioDraft withCell(SimpleStructureStudioCell updatedCell) { - Objects.requireNonNull(updatedCell, "updatedCell"); - requirePosition(updatedCell.x(), updatedCell.z()); - List updatedCells = new ArrayList<>(cells.size() + 1); - for (SimpleStructureStudioCell cell : cells) { - if (cell.x() != updatedCell.x() || cell.z() != updatedCell.z()) { - updatedCells.add(cell); - } - } - if (!updatedCell.isEmpty()) { - updatedCells.add(updatedCell); - } - return new SimpleStructureStudioDraft(layout, previewSeed, updatedCells); - } - - public SimpleStructureStudioDraft withoutCell(int x, int z) { - requirePosition(x, z); - List updatedCells = new ArrayList<>(cells.size()); - for (SimpleStructureStudioCell cell : cells) { - if (cell.x() != x || cell.z() != z) { - updatedCells.add(cell); - } - } - return new SimpleStructureStudioDraft(layout, previewSeed, updatedCells); - } - - private static void validateCells( - SimpleStructureStudioLayout layout, - List cells - ) { - Set positions = new HashSet<>(); - for (SimpleStructureStudioCell cell : cells) { - Objects.requireNonNull(cell, "cell"); - if (cell.isEmpty()) { - throw new IllegalArgumentException("Drafts store only populated cells"); - } - if (!layout.contains(cell.x(), cell.z())) { - throw new IllegalArgumentException( - "Cell is outside the studio grid: " + cell.x() + ", " + cell.z() - ); - } - if (cell.connectorHeight() >= layout.captureHeight()) { - throw new IllegalArgumentException( - "Connector height " + cell.connectorHeight() - + " is outside capture height " + layout.captureHeight() - ); - } - long position = ((long) cell.x() << 32) ^ (cell.z() & 0xffffffffL); - if (!positions.add(position)) { - throw new IllegalArgumentException("Duplicate studio cell: " + cell.x() + ", " + cell.z()); - } - } - } - - private void requirePosition(int x, int z) { - if (!layout.contains(x, z)) { - throw new IndexOutOfBoundsException("Cell is outside the studio grid: " + x + ", " + z); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioLayout.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioLayout.java deleted file mode 100644 index 99e0debdf..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioLayout.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -public record SimpleStructureStudioLayout( - int gridWidth, - int gridDepth, - int cellWidth, - int cellDepth, - int captureHeight -) { - public SimpleStructureStudioLayout { - requirePositive("gridWidth", gridWidth); - requirePositive("gridDepth", gridDepth); - requirePositive("cellWidth", cellWidth); - requirePositive("cellDepth", cellDepth); - requirePositive("captureHeight", captureHeight); - multiply("grid cell count", gridWidth, gridDepth); - multiply("studio width", gridWidth, cellWidth); - multiply("studio depth", gridDepth, cellDepth); - } - - public int cellCount() { - return gridWidth * gridDepth; - } - - public int studioWidth() { - return gridWidth * cellWidth; - } - - public int studioDepth() { - return gridDepth * cellDepth; - } - - public boolean contains(int x, int z) { - return x >= 0 && x < gridWidth && z >= 0 && z < gridDepth; - } - - private static void requirePositive(String name, int value) { - if (value <= 0) { - throw new IllegalArgumentException(name + " must be greater than zero: " + value); - } - } - - private static void multiply(String name, int first, int second) { - try { - Math.multiplyExact(first, second); - } catch (ArithmeticException e) { - throw new IllegalArgumentException(name + " exceeds the supported integer range", e); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioPublishConfig.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioPublishConfig.java deleted file mode 100644 index a90681cfa..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioPublishConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import art.arcane.iris.core.structure.authoring.StructureKey; -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; -import art.arcane.iris.engine.object.ObjectPlaceMode; - -import java.util.Objects; - -public record SimpleStructureStudioPublishConfig( - StructureKey structureKey, - String resourceKey, - int maxDepth, - int maxSizeChunks, - ObjectPlaceMode placeMode -) { - public SimpleStructureStudioPublishConfig { - Objects.requireNonNull(structureKey, "structureKey"); - Objects.requireNonNull(resourceKey, "resourceKey"); - Objects.requireNonNull(placeMode, "placeMode"); - if (resourceKey.isBlank()) { - throw new IllegalArgumentException("resourceKey cannot be blank"); - } - StructureResourceBundle.validateRelativePath("structures/" + resourceKey + ".json"); - if (!structureKey.path().equals(resourceKey)) { - throw new IllegalArgumentException("structureKey path must match resourceKey: " - + structureKey.path() + " != " + resourceKey); - } - if (maxDepth < 1 || maxDepth > 30) { - throw new IllegalArgumentException("maxDepth must be between 1 and 30: " + maxDepth); - } - if (maxSizeChunks < 1 || maxSizeChunks > 32) { - throw new IllegalArgumentException("maxSizeChunks must be between 1 and 32: " + maxSizeChunks); - } - } - - public static SimpleStructureStudioPublishConfig defaults( - StructureKey structureKey, - String resourceKey - ) { - return new SimpleStructureStudioPublishConfig( - structureKey, - resourceKey, - 7, - 8, - ObjectPlaceMode.STRUCTURE_PIECE - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepository.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepository.java deleted file mode 100644 index 570aa8b38..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepository.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import art.arcane.iris.core.structure.authoring.StructureKey; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.ReentrantLock; - -public final class SimpleStructureStudioRepository { - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private static final ConcurrentMap ROOT_LOCKS = new ConcurrentHashMap<>(); - - private final Path packRoot; - private final Path draftsRoot; - private final ReentrantLock rootLock; - - public SimpleStructureStudioRepository(Path packRoot) { - this.packRoot = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize(); - draftsRoot = this.packRoot.resolve(".iris/structure-studio").normalize(); - rootLock = ROOT_LOCKS.computeIfAbsent(this.packRoot, ignored -> new ReentrantLock()); - } - - public Path packRoot() { - return packRoot; - } - - public Path draftPath(StructureKey key) { - StructureKey activeKey = Objects.requireNonNull(key, "key"); - Path path = draftsRoot.resolve(activeKey.namespace()).resolve(activeKey.path() + ".json").normalize(); - if (!path.startsWith(draftsRoot)) { - throw new IllegalArgumentException("Studio draft key escapes the pack: " + key); - } - return path; - } - - public Optional load(StructureKey key) throws IOException { - Path target = draftPath(key); - rootLock.lock(); - try { - if (!Files.isRegularFile(target)) { - return Optional.empty(); - } - try { - SimpleStructureStudioDraft draft = GSON.fromJson( - Files.readString(target, StandardCharsets.UTF_8), SimpleStructureStudioDraft.class); - if (draft == null) { - throw new IOException("Studio draft is empty: " + target); - } - return Optional.of(draft); - } catch (RuntimeException e) { - throw new IOException("Invalid Studio draft " + target + ": " + e.getMessage(), e); - } - } finally { - rootLock.unlock(); - } - } - - public void save(StructureKey key, SimpleStructureStudioDraft draft) throws IOException { - Path target = draftPath(key); - byte[] content = GSON.toJson(Objects.requireNonNull(draft, "draft")).getBytes(StandardCharsets.UTF_8); - rootLock.lock(); - Path staged = target.resolveSibling(target.getFileName() + "." + UUID.randomUUID() + ".tmp"); - try { - Files.createDirectories(target.getParent()); - Files.write(staged, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); - moveReplace(staged, target); - } finally { - try { - Files.deleteIfExists(staged); - } finally { - rootLock.unlock(); - } - } - } - - private void moveReplace(Path source, Path target) throws IOException { - try { - Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException e) { - Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRotationPolicy.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRotationPolicy.java deleted file mode 100644 index 1c26fac18..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRotationPolicy.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -public enum SimpleStructureStudioRotationPolicy { - FIXED, - HALF_TURNS, - QUARTER_TURNS; - - public boolean allows(int quarterTurns) { - int normalizedTurns = Math.floorMod(quarterTurns, 4); - return switch (this) { - case FIXED -> normalizedTurns == 0; - case HALF_TURNS -> normalizedTurns == 0 || normalizedTurns == 2; - case QUARTER_TURNS -> true; - }; - } - - public int next(int quarterTurns) { - int normalizedTurns = Math.floorMod(quarterTurns, 4); - return switch (this) { - case FIXED -> 0; - case HALF_TURNS -> Math.floorMod(normalizedTurns + 2, 4); - case QUARTER_TURNS -> Math.floorMod(normalizedTurns + 1, 4); - }; - } - - public int previous(int quarterTurns) { - int normalizedTurns = Math.floorMod(quarterTurns, 4); - return switch (this) { - case FIXED -> 0; - case HALF_TURNS -> Math.floorMod(normalizedTurns - 2, 4); - case QUARTER_TURNS -> Math.floorMod(normalizedTurns - 1, 4); - }; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSession.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSession.java deleted file mode 100644 index b6be238ef..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSession.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Objects; -import java.util.function.UnaryOperator; - -public final class SimpleStructureStudioSession { - public static final int DEFAULT_HISTORY_LIMIT = 64; - public static final int MAX_HISTORY_LIMIT = 256; - - private static final long PREVIEW_SEED_STEP = 0x9E3779B97F4A7C15L; - - private final int historyLimit; - private final Deque undoHistory; - private final Deque redoHistory; - private SimpleStructureStudioDraft draft; - private SimpleStructureStudioDraft savedDraft; - - private SimpleStructureStudioSession(SimpleStructureStudioDraft draft, int historyLimit) { - this.draft = Objects.requireNonNull(draft, "draft"); - if (historyLimit <= 0 || historyLimit > MAX_HISTORY_LIMIT) { - throw new IllegalArgumentException( - "History limit must be between 1 and " + MAX_HISTORY_LIMIT + ": " + historyLimit - ); - } - this.historyLimit = historyLimit; - undoHistory = new ArrayDeque<>(historyLimit); - redoHistory = new ArrayDeque<>(historyLimit); - savedDraft = draft; - } - - public static SimpleStructureStudioSession open(SimpleStructureStudioDraft draft, int historyLimit) { - return new SimpleStructureStudioSession(draft, historyLimit); - } - - public static SimpleStructureStudioSession createNew(SimpleStructureStudioDraft draft, int historyLimit) { - SimpleStructureStudioSession session = new SimpleStructureStudioSession(draft, historyLimit); - session.savedDraft = null; - return session; - } - - public synchronized SimpleStructureStudioDraft draft() { - return draft; - } - - public int historyLimit() { - return historyLimit; - } - - public synchronized boolean isDirty() { - return savedDraft == null || !draft.equals(savedDraft); - } - - public synchronized boolean canUndo() { - return !undoHistory.isEmpty(); - } - - public synchronized boolean canRedo() { - return !redoHistory.isEmpty(); - } - - public synchronized int undoDepth() { - return undoHistory.size(); - } - - public synchronized int redoDepth() { - return redoHistory.size(); - } - - public synchronized void markSaved() { - savedDraft = draft; - } - - public synchronized boolean resize(SimpleStructureStudioLayout newLayout) { - Objects.requireNonNull(newLayout, "newLayout"); - if (draft.layout().equals(newLayout)) { - return false; - } - if (draft.hasContent()) { - throw new IllegalStateException("The studio layout cannot be resized after content has been added"); - } - return applyDraft(draft.withLayout(newLayout)); - } - - public synchronized boolean replaceCell(SimpleStructureStudioCell cell) { - return applyDraft(draft.withCell(Objects.requireNonNull(cell, "cell"))); - } - - public synchronized boolean clearCell(int x, int z) { - return applyDraft(draft.withoutCell(x, z)); - } - - public synchronized boolean setTopology(int x, int z, SimpleStructureStudioTopology topology) { - Objects.requireNonNull(topology, "topology"); - if (topology == SimpleStructureStudioTopology.EMPTY) { - return clearCell(x, z); - } - SimpleStructureStudioCell cell = draft.cellOrEmpty(x, z).withTopology(topology); - return applyDraft(draft.withCell(cell)); - } - - public synchronized boolean setQuarterTurns(int x, int z, int quarterTurns) { - return updatePopulatedCell(x, z, cell -> cell.withQuarterTurns(quarterTurns)); - } - - public synchronized boolean rotateClockwise(int x, int z) { - return updatePopulatedCell(x, z, SimpleStructureStudioCell::rotateClockwise); - } - - public synchronized boolean rotateCounterClockwise(int x, int z) { - return updatePopulatedCell(x, z, SimpleStructureStudioCell::rotateCounterClockwise); - } - - public synchronized boolean setRotationPolicy( - int x, - int z, - SimpleStructureStudioRotationPolicy rotationPolicy - ) { - Objects.requireNonNull(rotationPolicy, "rotationPolicy"); - return updatePopulatedCell(x, z, cell -> cell.withRotationPolicy(rotationPolicy)); - } - - public synchronized boolean setConnector(int x, int z, String channel, int height) { - return updatePopulatedCell(x, z, cell -> cell.withConnector(channel, height)); - } - - public synchronized boolean addVariant(int x, int z, SimpleStructureStudioVariant variant) { - Objects.requireNonNull(variant, "variant"); - return updatePopulatedCell(x, z, cell -> cell.addVariant(variant)); - } - - public synchronized boolean setVariantWeight(int x, int z, String variantId, int weight) { - return updatePopulatedCell(x, z, cell -> cell.setVariantWeight(variantId, weight)); - } - - public synchronized boolean removeVariant(int x, int z, String variantId) { - return updatePopulatedCell(x, z, cell -> cell.removeVariant(variantId)); - } - - public synchronized boolean selectVariant(int x, int z, String variantId) { - return updatePopulatedCell(x, z, cell -> cell.selectVariant(variantId)); - } - - public synchronized boolean cycleVariant(int x, int z, int offset) { - return updatePopulatedCell(x, z, cell -> cell.cycleVariant(offset)); - } - - public synchronized boolean setPreviewSeed(long previewSeed) { - return applyDraft(draft.withPreviewSeed(previewSeed)); - } - - public synchronized long advancePreviewSeed() { - long nextSeed = nextPreviewSeed(draft.previewSeed()); - applyDraft(draft.withPreviewSeed(nextSeed)); - return nextSeed; - } - - public synchronized boolean undo() { - if (undoHistory.isEmpty()) { - return false; - } - redoHistory.addLast(draft); - trimHistory(redoHistory); - draft = undoHistory.removeLast(); - return true; - } - - public synchronized boolean redo() { - if (redoHistory.isEmpty()) { - return false; - } - undoHistory.addLast(draft); - trimHistory(undoHistory); - draft = redoHistory.removeLast(); - return true; - } - - public static long nextPreviewSeed(long previewSeed) { - return previewSeed + PREVIEW_SEED_STEP; - } - - private boolean updatePopulatedCell( - int x, - int z, - UnaryOperator update - ) { - SimpleStructureStudioCell cell = draft.cellAt(x, z).orElseThrow( - () -> new IllegalStateException("Studio cell is empty: " + x + ", " + z) - ); - SimpleStructureStudioCell updatedCell = Objects.requireNonNull(update.apply(cell), "updatedCell"); - if (updatedCell.x() != x || updatedCell.z() != z) { - throw new IllegalArgumentException("Cell updates cannot change the cell position"); - } - return applyDraft(draft.withCell(updatedCell)); - } - - private boolean applyDraft(SimpleStructureStudioDraft updatedDraft) { - Objects.requireNonNull(updatedDraft, "updatedDraft"); - if (draft.equals(updatedDraft)) { - return false; - } - undoHistory.addLast(draft); - trimHistory(undoHistory); - draft = updatedDraft; - redoHistory.clear(); - return true; - } - - private void trimHistory(Deque history) { - while (history.size() > historyLimit) { - history.removeFirst(); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioTopology.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioTopology.java deleted file mode 100644 index b62de694a..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioTopology.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -public enum SimpleStructureStudioTopology { - EMPTY(0), - END(SimpleStructureStudioDirection.NORTH.mask()), - STRAIGHT(SimpleStructureStudioDirection.NORTH.mask() | SimpleStructureStudioDirection.SOUTH.mask()), - CORNER(SimpleStructureStudioDirection.NORTH.mask() | SimpleStructureStudioDirection.EAST.mask()), - T(SimpleStructureStudioDirection.NORTH.mask() - | SimpleStructureStudioDirection.EAST.mask() - | SimpleStructureStudioDirection.WEST.mask()), - CROSS(SimpleStructureStudioDirection.ALL_MASK), - START(SimpleStructureStudioDirection.NORTH.mask()), - TERMINAL(SimpleStructureStudioDirection.NORTH.mask()); - - private final int baseConnectorMask; - - SimpleStructureStudioTopology(int baseConnectorMask) { - this.baseConnectorMask = baseConnectorMask; - } - - public int baseConnectorMask() { - return baseConnectorMask; - } - - public int connectorMask(int quarterTurns) { - return SimpleStructureStudioDirection.rotateMask(baseConnectorMask, quarterTurns); - } - - public int connectorCount() { - return Integer.bitCount(baseConnectorMask); - } - - public boolean connects(SimpleStructureStudioDirection direction, int quarterTurns) { - return (connectorMask(quarterTurns) & direction.mask()) != 0; - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariant.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariant.java deleted file mode 100644 index 765d125ee..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariant.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; - -import java.util.Objects; -import java.util.regex.Pattern; - -public record SimpleStructureStudioVariant(String id, int weight) { - private static final Pattern ID_PATTERN = Pattern.compile("[a-z0-9._-]+(?:/[a-z0-9._-]+)*"); - - public SimpleStructureStudioVariant { - Objects.requireNonNull(id, "id"); - if (!ID_PATTERN.matcher(id).matches()) { - throw new IllegalArgumentException("Variant id must be a portable lowercase resource path: " + id); - } - StructureResourceBundle.validateRelativePath(id); - if (weight <= 0) { - throw new IllegalArgumentException("Variant weight must be greater than zero: " + weight); - } - } - - public SimpleStructureStudioVariant withWeight(int newWeight) { - return new SimpleStructureStudioVariant(id, newWeight); - } -} diff --git a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariantKey.java b/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariantKey.java deleted file mode 100644 index 841f7dde3..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioVariantKey.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import java.util.Objects; - -public record SimpleStructureStudioVariantKey(int cellX, int cellZ, String variantId) { - public SimpleStructureStudioVariantKey { - if (cellX < 0 || cellZ < 0) { - throw new IllegalArgumentException("Variant cell coordinates cannot be negative: " + cellX + ", " + cellZ); - } - Objects.requireNonNull(variantId, "variantId"); - if (variantId.isBlank()) { - throw new IllegalArgumentException("Variant id cannot be blank"); - } - } - - public static SimpleStructureStudioVariantKey of( - SimpleStructureStudioCell cell, - SimpleStructureStudioVariant variant - ) { - Objects.requireNonNull(cell, "cell"); - Objects.requireNonNull(variant, "variant"); - return new SimpleStructureStudioVariantKey(cell.x(), cell.z(), variant.id()); - } -} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java index 2bb73c202..fdcd1b3d1 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java @@ -162,13 +162,14 @@ public class IrisPackBenchmarking { } private double calculateMedian(KList list) { - Collections.sort(list); - int middle = list.size() / 2; + KList sorted = new KList<>(list); + Collections.sort(sorted); + int middle = sorted.size() / 2; - if (list.size() % 2 == 1) { - return list.get(middle); + if (sorted.size() % 2 == 1) { + return sorted.get(middle); } else { - return (list.get(middle - 1) + list.get(middle)) / 2.0; + return (sorted.get(middle - 1) + sorted.get(middle)) / 2.0; } } diff --git a/core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java b/core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java new file mode 100644 index 000000000..c32496c9e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineBackgroundTasks.java @@ -0,0 +1,158 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.util.common.scheduling.J; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static art.arcane.iris.engine.EngineShutdownSequence.appendFailure; +import static art.arcane.iris.engine.EngineShutdownSequence.propagate; + +/** + * Admission gate and drain tracker for the asynchronous work an {@link IrisEngine} schedules. + * Admission is closed before any lifecycle transition so a draining engine can never accrue new + * background work, and every tracked task is awaited (or cancelled) before resources are released. + */ +final class EngineBackgroundTasks { + private static final long BACKGROUND_TASK_TIMEOUT_MILLIS = 15000L; + + private final Object backgroundTaskLock = new Object(); + private final List backgroundTasks = new ArrayList<>(); + private boolean backgroundTaskAdmission; + + boolean scheduleTrackedTask(Runnable task) { + synchronized (backgroundTaskLock) { + backgroundTasks.removeIf(tracked -> tracked.completion.isDone() + && !tracked.completion.isCompletedExceptionally()); + if (!backgroundTaskAdmission) { + return false; + } + TrackedBackgroundTask tracked = new TrackedBackgroundTask(); + Future future = J.a(() -> { + tracked.started.set(true); + try { + task.run(); + tracked.completion.complete(null); + return null; + } catch (Throwable exception) { + tracked.completion.completeExceptionally(exception); + throw propagate(exception); + } + }); + if (future == null) { + throw new IllegalStateException("Iris background task scheduler returned no task handle."); + } + tracked.future = future; + backgroundTasks.add(tracked); + return true; + } + } + + BackgroundTaskDrain drainBackgroundTasks(String reason) { + List tasks; + synchronized (backgroundTaskLock) { + tasks = List.copyOf(backgroundTasks); + } + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS); + Throwable failure = null; + for (TrackedBackgroundTask task : tasks) { + long remaining = deadline - System.nanoTime(); + if (remaining <= 0L) { + cancelBackgroundTask(task, reason); + failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + ".")); + continue; + } + try { + task.completion.get(remaining, TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + cancelBackgroundTask(task, reason); + failure = appendFailure(failure, e); + } catch (ExecutionException | TimeoutException e) { + cancelBackgroundTask(task, reason); + failure = appendFailure(failure, e); + } + } + boolean complete; + synchronized (backgroundTaskLock) { + backgroundTasks.removeIf(tracked -> tracked.completion.isDone()); + complete = backgroundTasks.isEmpty(); + } + return new BackgroundTaskDrain(failure, complete); + } + + private void cancelBackgroundTask(TrackedBackgroundTask task, String reason) { + Future future = task.future; + if (future != null && future.cancel(true) && !task.started.get()) { + task.completion.completeExceptionally( + new IllegalStateException("Iris background task was cancelled before starting during " + reason + ".")); + } + } + + void openBackgroundTaskAdmission() { + synchronized (backgroundTaskLock) { + backgroundTaskAdmission = true; + } + } + + void closeBackgroundTaskAdmission() { + synchronized (backgroundTaskLock) { + backgroundTaskAdmission = false; + } + } + + void cancelBackgroundTasks(String reason) { + List tasks; + synchronized (backgroundTaskLock) { + tasks = List.copyOf(backgroundTasks); + } + for (TrackedBackgroundTask task : tasks) { + cancelBackgroundTask(task, reason); + } + } + + private static final class TrackedBackgroundTask { + private final AtomicBoolean started = new AtomicBoolean(); + private final CompletableFuture completion = new CompletableFuture<>(); + private volatile Future future; + } + + record BackgroundTaskDrain(Throwable failure, boolean complete) { + boolean allowsResourceRelease() { + return complete; + } + + void requireComplete(String reason) { + if (failure != null) { + throw new IllegalStateException("Iris background tasks failed to drain during " + reason + ".", failure); + } + if (!complete) { + throw new IllegalStateException("Iris background tasks remain active during " + reason + "."); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineDataStore.java b/core/src/main/java/art/arcane/iris/engine/EngineDataStore.java new file mode 100644 index 000000000..9e5f2480a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineDataStore.java @@ -0,0 +1,138 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.object.IrisEngineData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.volmlib.util.io.IO; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * On-disk persistence for a single engine's {@link IrisEngineData}. Loads are double-checked against + * the engine's volatile field and serialized on a dedicated lock, and every write goes through an + * atomic temp-file move so a crash mid-save can never truncate the live engine data. + */ +final class EngineDataStore { + private final IrisEngine engine; + private final Object engineDataLock = new Object(); + + EngineDataStore(IrisEngine engine) { + this.engine = engine; + } + + IrisEngineData getEngineData() { + IrisEngineData loaded = engine.engineData; + if (loaded != null) { + return loaded; + } + synchronized (engineDataLock) { + loaded = engine.engineData; + if (loaded != null) { + return loaded; + } + File f = new File(engine.getWorld().worldFolder(), "iris/engine-data/" + engine.getDimension().getLoadKey() + ".json"); + if (f.exists()) { + try { + loaded = new Gson().fromJson(IO.readAll(f), IrisEngineData.class); + if (loaded == null) { + throw new IllegalStateException("Engine data file contains no JSON object: " + f.getAbsolutePath()); + } + } catch (IOException | JsonParseException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + throw new IllegalStateException("Failed to read Iris engine data without modifying it: " + f.getAbsolutePath(), e); + } + } + + if (loaded == null) { + loaded = new IrisEngineData(); + loaded.getStatistics().setVersion(IrisPlatforms.get().irisVersionNumber()); + loaded.getStatistics().setMCVersion(IrisPlatforms.get().minecraftVersionNumber()); + loaded.getStatistics().setUpgradedVersion(IrisPlatforms.get().irisVersionNumber()); + if (loaded.getStatistics().getVersion() == -1 || loaded.getStatistics().getMCVersion() == -1) { + IrisLogging.error("Failed to setup Engine Data!"); + } + try { + writeEngineDataAtomically(f, loaded); + } catch (IOException e) { + IrisLogging.reportError(e); + e.printStackTrace(); + throw new IllegalStateException("Failed to create Iris engine data: " + f.getAbsolutePath(), e); + } + } + engine.engineData = loaded; + return loaded; + } + } + + void saveEngineData() { + synchronized (engineDataLock) { + File f = new File(engine.getWorld().worldFolder(), "iris/engine-data/" + engine.getDimension().getLoadKey() + ".json"); + try { + writeEngineDataAtomically(f, engine.getEngineData()); + IrisLogging.debug("Saved Engine Data"); + } catch (IOException e) { + IrisLogging.error("Failed to save Engine Data"); + IrisLogging.reportError(e); + e.printStackTrace(); + throw new IllegalStateException("Failed to save Iris engine data: " + f.getAbsolutePath(), e); + } + } + } + + void releaseEngineData() { + IrisData data = engine.getData(); + data.unregisterEngine(engine); + if (data.getEngines().isEmpty()) { + data.close(); + data.clearLists(); + } + } + + static void writeEngineDataAtomically(File file, IrisEngineData data) throws IOException { + Path output = file.toPath(); + Path parent = output.getParent(); + if (parent == null) { + throw new IOException("Engine data path has no parent: " + output); + } + Files.createDirectories(parent); + Path temporary = Files.createTempFile(parent, output.getFileName().toString(), ".tmp"); + try { + Files.writeString(temporary, new Gson().toJson(data), StandardCharsets.UTF_8); + try { + Files.move(temporary, output, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temporary, output, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temporary); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java b/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java new file mode 100644 index 000000000..6c5d6e5a0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineHotloader.java @@ -0,0 +1,194 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.protocol.IrisProtocolServer; +import art.arcane.iris.engine.EngineRuntime.BiomeMaxes; +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.engine.IrisEngine.LifecycleState; +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.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.protocol.IrisMessage; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.RNG; + +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. + */ +final class EngineHotloader { + private final IrisEngine engine; + + EngineHotloader(IrisEngine engine) { + this.engine = engine; + } + + void hotload() { + hotloadSilently(); + engine.getPlatformHooks().fireHotloadEvent(engine); + } + + void hotloadComplex() { + synchronized (engine.lifecycleLock) { + engine.requireRunning("rebuild the biome complex"); + engine.lifecycleState = LifecycleState.HOTLOADING; + EngineRuntime previous = engine.runtime; + IrisComplex nextComplex = null; + try { + engine.sealForTransition("complex hotload", false); + RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), previous.target()); + engine.runtimeAssembly.set(assembly); + EngineRuntime next; + try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { + assembly.complex = new IrisComplex(engine); + nextComplex = assembly.complex; + assembly.upperContext = engine.runtimeBuilder.buildUpperContext(); + BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes(); + next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes); + } finally { + engine.runtimeAssembly.remove(); + } + Throwable retirementFailure = runCleanup(null, previous.complex()::close); + if (retirementFailure != null) { + retirementFailure = runCleanup(retirementFailure, nextComplex::close); + nextComplex = null; + engine.lifecycleState = LifecycleState.FAILED; + throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure); + } + engine.runtime = next; + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + } catch (Throwable e) { + if (nextComplex != null && nextComplex != previous.complex()) { + Throwable cleanupFailure = runCleanup(null, nextComplex::close); + if (cleanupFailure != null) { + e.addSuppressed(cleanupFailure); + } + } + if (engine.lifecycleState != LifecycleState.FAILED) { + engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous); + } + throw new IllegalStateException("Failed to rebuild the Iris biome complex.", e); + } + } + } + + void hotloadSilently() { + synchronized (engine.lifecycleLock) { + engine.requireRunning("hotload"); + engine.lifecycleState = LifecycleState.HOTLOADING; + EngineRuntime previousRuntime = engine.runtime; + IrisDimension previousDimension = engine.getDimension(); + IrisData previousData = engine.getData(); + IrisData replacementData = null; + boolean published = false; + try { + engine.sealForTransition("hotload", false); + replacementData = IrisData.openRuntime(previousData.getDataFolder()); + IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey()); + if (replacement == null) { + throw new IllegalStateException("Studio hotload could not reload Iris dimension '" + previousDimension.getLoadKey() + "'"); + } + engine.getPlatformHooks().validateDimensionHotload(engine, replacement); + replacementData.registerEngine(engine); + IrisStructureLocator.invalidate(engine); + StructureReachability.invalidate(engine); + EngineTarget replacementTarget = new EngineTarget(engine.getWorld(), replacement, replacementData); + EngineRuntime nextRuntime = engine.runtimeBuilder.buildRuntime(replacementTarget); + engine.runtimeBuilder.publishRuntime(nextRuntime, previousRuntime); + published = true; + previousData.unregisterEngine(engine); + Throwable previousDataFailure = runCleanup(null, previousData::close); + if (previousDataFailure != null) { + IrisLogging.error("Failed to completely release the previous Iris data runtime."); + IrisLogging.reportError(previousDataFailure); + previousDataFailure.printStackTrace(); + } + engine.getPrefetchSaveStarted().set(false); + engine.getEngineData().getStatistics().hotloaded(); + if (engine.getWorld().hasPlatformWorld()) { + if (!engine.backgroundTasks.scheduleTrackedTask(() -> { + engine.getPlatformHooks().refreshWorkspace(engine); + engine.getPlatformHooks().reloadDatapacks(engine); + })) { + throw new IllegalStateException("Iris background task admission closed before workspace refresh."); + } + } + broadcastStudioHotload(false, ""); + } catch (Throwable e) { + if (!published) { + if (replacementData != null) { + replacementData.unregisterEngine(engine); + Throwable replacementDataFailure = runCleanup(null, replacementData::close); + if (replacementDataFailure != null) { + e.addSuppressed(replacementDataFailure); + } + } + IrisStructureLocator.invalidate(engine); + StructureReachability.invalidate(engine); + if (engine.lifecycleState != LifecycleState.FAILED) { + Throwable rollbackFailure = runCleanup(null, engine.getMantle()::hotload); + if (rollbackFailure == null) { + engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previousRuntime); + } else { + engine.runtime = previousRuntime; + engine.lifecycleState = LifecycleState.FAILED; + e.addSuppressed(rollbackFailure); + } + } + } + broadcastStudioHotload(true, e.getClass().getSimpleName() + ": " + e.getMessage()); + if (e instanceof Error error) { + throw error; + } + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException("Iris hotload failed.", e); + } + } + } + + private void broadcastStudioHotload(boolean failed, String message) { + IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); + if (protocolServer == null) { + return; + } + IrisDimension dimension = engine.getDimension(); + String packKey = dimension == null ? "" : dimension.getLoadKey(); + protocolServer.broadcastStudioHotload(packKey, 0, failed, message); + protocolServer.broadcastToast( + failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS, + IrisLanguage.plain(ClientUiMessages.TOAST_STUDIO_HOTLOAD), + failed ? IrisLanguage.plain(ClientUiMessages.TOAST_PACK_FAILED, MessageArgument.untrusted("pack", packKey)) : packKey); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java b/core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java new file mode 100644 index 000000000..ce4c5e190 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java @@ -0,0 +1,112 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.localization.BukkitRuntimeMessages; +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.localization.MessageArgument; +import art.arcane.volmlib.util.math.M; + +/** + * Derives human readable timing reports and the rolling generation rate for an {@link IrisEngine}. + * Purely a reader of the engine's metric accumulators; it never mutates generation state. + */ +final class EngineMetricsReport { + private final IrisEngine engine; + + EngineMetricsReport(IrisEngine engine) { + this.engine = engine; + } + + void printMetrics(VolmitSender sender) { + KMap totals = new KMap<>(); + KMap weights = new KMap<>(); + double masterWallClock = engine.getWallClock().getAverage(); + KMap timings = engine.getMetrics().pull(); + double totalWeight = 0; + double wallClock = engine.getMetrics().getTotal().getAverage(); + + for (double j : timings.values()) { + totalWeight += j; + } + + for (String j : timings.k()) { + weights.put(engine.getName() + "." + j, (wallClock / totalWeight) * timings.get(j)); + } + + totals.put(engine.getName(), wallClock); + + double mtotals = 0; + + for (double i : totals.values()) { + mtotals += i; + } + + for (String i : totals.k()) { + totals.put(i, (masterWallClock / mtotals) * totals.get(i)); + } + + double v = 0; + + for (double i : weights.values()) { + v += i; + } + + for (String i : weights.k()) { + weights.put(i, weights.get(i) / v); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_TOTAL, MessageArgument.untrusted("value", String.valueOf(Form.duration(masterWallClock, 0))))); + + for (String i : totals.k()) { + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_ENGINE, MessageArgument.untrusted("i", String.valueOf(i)), MessageArgument.untrusted("value", String.valueOf(Form.duration(totals.get(i), 0))))); + } + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_DETAILS)); + + for (String i : weights.sortKNumber().reverse()) { + String befb = C.UNDERLINE + "" + C.GREEN + "" + i.split("\\Q[\\E")[0] + C.RESET + C.GRAY + "["; + String num = C.GOLD + i.split("\\Q[\\E")[1].split("]")[0] + C.RESET + C.GRAY + "]."; + String afb = C.ITALIC + "" + C.AQUA + i.split("\\Q]\\E")[1].substring(1) + C.RESET + C.GRAY; + + sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_MESSAGE, MessageArgument.untrusted("befb", String.valueOf(befb)), MessageArgument.untrusted("num", String.valueOf(num)), MessageArgument.untrusted("afb", String.valueOf(afb)), MessageArgument.untrusted("value", String.valueOf(Form.pc(weights.get(i), 0))))); + } + } + + double getGeneratedPerSecond() { + if (engine.getPerSecondLatch().flip()) { + double g = engine.getGenerated() - engine.getGeneratedLast().get(); + engine.getGeneratedLast().set(engine.getGenerated()); + + if (g == 0) { + return 0; + } + + long dur = M.ms() - engine.getLastGPS().get(); + engine.getLastGPS().set(M.ms()); + engine.getPerSecond().set(g / ((double) (dur) / 1000D)); + } + + return engine.getPerSecond().get(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineRuntime.java b/core/src/main/java/art/arcane/iris/engine/EngineRuntime.java new file mode 100644 index 000000000..1686edc9d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineRuntime.java @@ -0,0 +1,63 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; + +import java.util.concurrent.CompletableFuture; + +/** + * Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit. + * Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}. + */ +record EngineRuntime( + int cacheId, + EngineTarget target, + IrisComplex complex, + UpperDimensionContext upperContext, + EngineEffects effects, + EngineMode mode, + EngineWorldManager worldManager, + CompletableFuture hash32, + BiomeMaxes biomeMaxes +) { + EngineRuntime withComplex( + int nextCacheId, + IrisComplex nextComplex, + UpperDimensionContext nextUpperContext, + BiomeMaxes nextBiomeMaxes + ) { + return new EngineRuntime( + nextCacheId, + target, + nextComplex, + nextUpperContext, + effects, + mode, + worldManager, + hash32, + nextBiomeMaxes); + } + + record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) { + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java b/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java new file mode 100644 index 000000000..c5d55501a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java @@ -0,0 +1,312 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.EngineRuntime.BiomeMaxes; +import art.arcane.iris.engine.IrisEngine.LifecycleState; +import art.arcane.iris.engine.framework.EngineEffects; +import art.arcane.iris.engine.framework.EngineEffectsProvider; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.framework.EngineTarget; +import art.arcane.iris.engine.framework.EngineWorldManager; +import art.arcane.iris.engine.framework.EngineWorldManagerProvider; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomePaletteLayer; +import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisDimensionMode; +import art.arcane.iris.engine.object.IrisDimensionModeType; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.util.project.context.IrisContext; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.math.M; +import art.arcane.volmlib.util.math.RNG; + +import java.io.File; +import java.util.concurrent.CompletableFuture; + +import static art.arcane.iris.engine.EngineShutdownSequence.propagate; + +/** + * Assembles and publishes {@link EngineRuntime} snapshots for an {@link IrisEngine}. + * A runtime is built into a thread-local {@link RuntimeAssembly} so partially constructed state is + * visible only to the building thread, then frozen and published to the engine's volatile runtime + * field as a single atomic swap. + */ +final class EngineRuntimeBuilder { + private final IrisEngine engine; + + EngineRuntimeBuilder(IrisEngine engine) { + this.engine = engine; + } + + EngineRuntime buildRuntime() { + return buildRuntime(engine.getTarget()); + } + + EngineRuntime buildRuntime(EngineTarget runtimeTarget) { + RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), runtimeTarget); + engine.runtimeAssembly.set(assembly); + try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { + IrisLogging.debug("Setup Engine " + assembly.cacheId); + long started = M.ms(); + assembly.complex = new IrisComplex(engine); + IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.upperContext = buildUpperContext(); + IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine); + if (assembly.effects == null) { + throw new IllegalStateException("Engine effects provider returned null"); + } + IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms"); + assembly.hash32 = new CompletableFuture<>(); + started = M.ms(); + engine.getMantle().hotload(); + IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.mode = createMode(); + IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms"); + started = M.ms(); + assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine); + if (assembly.worldManager == null) { + throw new IllegalStateException("Engine world manager provider returned null"); + } + IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms"); + BiomeMaxes biomeMaxes = computeBiomeMaxes(); + return assembly.freeze(biomeMaxes); + } catch (Throwable e) { + Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e); + if (cleanupFailure != e) { + e.addSuppressed(cleanupFailure); + } + throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e); + } finally { + engine.runtimeAssembly.remove(); + } + } + + void publishRuntime(EngineRuntime next, EngineRuntime previous) { + Throwable retirementFailure = engine.shutdownSequence.closeRuntime(previous, null); + if (retirementFailure != null) { + retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure); + engine.lifecycleState = LifecycleState.FAILED; + throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure); + } + if (engine.runtime == previous) { + engine.runtime = null; + } + + try { + next.worldManager().start(); + } catch (Throwable e) { + Throwable cleanupFailure = engine.shutdownSequence.closeRuntime(next, e); + if (cleanupFailure != e) { + e.addSuppressed(cleanupFailure); + } + engine.lifecycleState = LifecycleState.FAILED; + throw new IllegalStateException("Failed to start the Iris world manager.", e); + } + + engine.runtime = next; + engine.publishedTarget = next.target(); + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + scheduleRuntimeTasks(next); + IrisLogging.debug("Engine Setup Complete " + next.cacheId()); + } + + private void scheduleRuntimeTasks(EngineRuntime engineRuntime) { + try { + if (!engine.backgroundTasks.scheduleTrackedTask(() -> { + try { + File[] roots = engine.getData().getLoaders() + .values() + .stream() + .map(ResourceLoader::getFolderName) + .map(name -> new File(engine.getData().getDataFolder(), name)) + .filter(File::exists) + .filter(File::isDirectory) + .toArray(File[]::new); + engineRuntime.hash32().complete(IO.hashRecursiveMeta(roots)); + } catch (Throwable e) { + engineRuntime.hash32().completeExceptionally(e); + throw propagate(e); + } + })) { + throw new IllegalStateException("Iris background task admission closed before pack hashing."); + } + } catch (Throwable e) { + engineRuntime.hash32().completeExceptionally(e); + IrisLogging.reportError(e); + e.printStackTrace(); + } + try { + if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) { + throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh."); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + UpperDimensionContext buildUpperContext() { + IrisDimension dim = engine.getDimension(); + if (!dim.hasUpperDimension()) { + return null; + } + String upperKey = dim.getUpperDimension(); + IrisDimension upperDim = upperKey.equals(dim.getLoadKey()) + ? dim + : IrisData.loadAnyDimension(upperKey, engine.getData()); + if (upperDim != null) { + UpperDimensionContext ctx = UpperDimensionContext.create(engine, upperDim); + IrisLogging.info("Upper dimension enabled: " + upperKey + + (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)")); + return ctx; + } + IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain."); + return null; + } + + private EngineMode createMode() { + Throwable configuredFailure = null; + try { + IrisDimensionMode configuredMode = engine.getDimension().getMode(); + if (configuredMode == null) { + configuredMode = new IrisDimensionMode(); + engine.getDimension().setMode(configuredMode); + } + EngineMode configured = configuredMode.create(engine); + if (configured == null) { + throw new IllegalStateException("Dimension mode factory returned null"); + } + return configured; + } catch (Throwable e) { + configuredFailure = e; + IrisLogging.reportError(e); + e.printStackTrace(); + if (engine.getModeFallbackLogged().compareAndSet(false, true)) { + IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode."); + } + } + + try { + EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine); + if (fallback == null) { + throw new IllegalStateException("OVERWORLD mode factory returned null"); + } + return fallback; + } catch (Throwable fallbackFailure) { + fallbackFailure.addSuppressed(configuredFailure); + throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure); + } + } + + BiomeMaxes computeBiomeMaxes() { + double objectDensity = 0D; + double layerDensity = 0D; + double decoratorDensity = 0D; + for (IrisBiome i : engine.getDimension().getReachableBiomes(engine)) { + double density = 0; + + for (IrisObjectPlacement j : i.getObjects()) { + density += j.getDensity() * j.getChance(); + } + + objectDensity = Math.max(objectDensity, density); + density = 0; + + for (IrisDecorator j : i.getDecorators()) { + density += Math.max(j.getStackMax(), 1) * j.getChance(); + } + + decoratorDensity = Math.max(decoratorDensity, density); + density = 0; + + for (IrisBiomePaletteLayer j : i.getLayers()) { + density++; + } + + layerDensity = Math.max(layerDensity, density); + } + return new BiomeMaxes(objectDensity, layerDensity, decoratorDensity); + } + + void restoreRuntimeAfterFailedTransition(EngineRuntime previous) { + engine.runtime = previous; + if (engine.getGenerationSessions().activeLeases() == 0) { + engine.getGenerationSessions().activateNextSession(); + engine.lifecycleState = LifecycleState.RUNNING; + engine.getClosing().set(false); + engine.backgroundTasks.openBackgroundTaskAdmission(); + return; + } + engine.lifecycleState = LifecycleState.FAILED; + } + + EngineRuntime requireRuntime(String operation) { + EngineRuntime current = engine.runtime; + if (current == null) { + throw new IllegalStateException("Cannot " + operation + " without an active Iris runtime for " + + engine.getWorld().name() + "."); + } + return current; + } + + static final class RuntimeAssembly { + final int cacheId; + final EngineTarget target; + IrisComplex complex; + UpperDimensionContext upperContext; + EngineEffects effects; + EngineMode mode; + EngineWorldManager worldManager; + CompletableFuture hash32; + + RuntimeAssembly(int cacheId, EngineTarget target) { + this.cacheId = cacheId; + this.target = target; + } + + EngineRuntime freeze(BiomeMaxes biomeMaxes) { + if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) { + throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime."); + } + return new EngineRuntime( + cacheId, + target, + complex, + upperContext, + effects, + mode, + worldManager, + hash32, + biomeMaxes); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java b/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java new file mode 100644 index 000000000..daae0963b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java @@ -0,0 +1,291 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain; +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.engine.IrisEngine.LifecycleState; +import art.arcane.iris.engine.framework.GenerationSessionException; +import art.arcane.iris.engine.framework.PreservationRegistry; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisServices; + +/** + * Owns the ordered teardown of a single {@link IrisEngine}, both for a normal close and for a + * construction that failed part way through. Every resource release is gated on the previous one + * having succeeded so a partially released engine never reports itself as closed. + */ +final class EngineShutdownSequence { + private final IrisEngine engine; + private boolean runtimeReleased; + private boolean targetReleased; + private boolean mantleReleased; + private boolean engineDataReleased; + private boolean preservationReleased; + + EngineShutdownSequence(IrisEngine engine) { + this.engine = engine; + } + + void close() { + Throwable failure; + synchronized (engine.lifecycleLock) { + if (engine.closed) { + return; + } + engine.lifecycleState = LifecycleState.CLOSING; + engine.getClosing().set(true); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + EngineTickRegistry.unregisterTicking(engine); + engine.getPlatformHooks().shutdownPregenerator(engine); + try { + engine.getGenerationSessions().sealAndAwait("close", IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS, true); + } catch (GenerationSessionException e) { + throw new IllegalStateException("Failed to drain Iris generation for close.", e); + } + + BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("close"); + failure = backgroundDrain.failure(); + if (!backgroundDrain.allowsResourceRelease() && failure == null) { + failure = new IllegalStateException("Iris background tasks remain active during close."); + } + + if (backgroundDrain.allowsResourceRelease()) { + Throwable prefetchFailure = runCleanup(null, engine::savePrefetchOnce); + Throwable engineDataFailure = runCleanup(null, engine::saveEngineData); + failure = appendFailure(failure, prefetchFailure); + failure = appendFailure(failure, engineDataFailure); + failure = releaseRuntime(failure); + if (runtimeReleased) { + failure = releaseTarget(failure); + } + if (targetReleased) { + failure = releaseMantle(failure); + } + if (prefetchFailure == null + && engineDataFailure == null + && runtimeReleased + && targetReleased + && mantleReleased) { + failure = releaseEngineDataForShutdown(failure); + } + if (engineDataReleased) { + failure = releasePreservation(failure); + } + } + if (failure == null + && runtimeReleased + && targetReleased + && mantleReleased + && engineDataReleased + && preservationReleased) { + engine.closed = true; + engine.lifecycleState = LifecycleState.CLOSED; + IrisLogging.debug("Engine Fully Shutdown!"); + } + } + if (failure != null) { + IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + "."); + IrisLogging.reportError(failure); + failure.printStackTrace(); + throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure); + } + } + + void cleanupFailedConstruction(Throwable original) { + engine.getClosing().set(true); + engine.backgroundTasks.closeBackgroundTaskAdmission(); + EngineTickRegistry.unregisterTicking(engine); + engine.lifecycleState = LifecycleState.FAILED; + Throwable cleanupFailure = null; + try { + engine.getGenerationSessions().sealAndAwait("failed initialization", 0L, true); + } catch (Throwable e) { + cleanupFailure = appendFailure(cleanupFailure, e); + } + engine.backgroundTasks.cancelBackgroundTasks("failed initialization"); + BackgroundTaskDrain backgroundDrain = engine.backgroundTasks.drainBackgroundTasks("failed initialization"); + cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure()); + if (!backgroundDrain.allowsResourceRelease()) { + cleanupFailure = appendFailure(cleanupFailure, + new IllegalStateException("Iris background tasks remain active after failed initialization.")); + if (cleanupFailure != original) { + original.addSuppressed(cleanupFailure); + } + return; + } + cleanupFailure = closeRuntime(engine.runtime, cleanupFailure); + engine.runtime = null; + cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close); + cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close); + cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData); + engine.closed = true; + cleanupFailure = runCleanup(cleanupFailure, () -> { + PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); + if (registry != null) { + registry.dereference(); + } + }); + if (cleanupFailure != null && cleanupFailure != original) { + original.addSuppressed(cleanupFailure); + } + } + + Throwable closeAssembly(RuntimeAssembly assembly, Throwable failure) { + if (assembly == null) { + return failure; + } + failure = runCleanup(failure, () -> { + if (assembly.worldManager != null) { + assembly.worldManager.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.effects != null) { + assembly.effects.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.mode != null) { + assembly.mode.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.complex != null) { + assembly.complex.close(); + } + }); + failure = runCleanup(failure, () -> { + if (assembly.hash32 != null) { + assembly.hash32.cancel(true); + } + }); + return failure; + } + + Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) { + if (engineRuntime == null) { + return failure; + } + failure = runCleanup(failure, engineRuntime.worldManager()::close); + failure = runCleanup(failure, engineRuntime.effects()::close); + failure = runCleanup(failure, engineRuntime.mode()::close); + failure = runCleanup(failure, engineRuntime.complex()::close); + failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true)); + return failure; + } + + private Throwable releaseRuntime(Throwable failure) { + if (runtimeReleased) { + return failure; + } + Throwable runtimeFailure = closeRuntime(engine.runtime, null); + if (runtimeFailure != null) { + return appendFailure(failure, runtimeFailure); + } + engine.runtime = null; + runtimeReleased = true; + return failure; + } + + private Throwable releaseTarget(Throwable failure) { + if (targetReleased) { + return failure; + } + Throwable targetFailure = runCleanup(null, engine.getTarget()::close); + if (targetFailure != null) { + return appendFailure(failure, targetFailure); + } + targetReleased = true; + return failure; + } + + private Throwable releaseMantle(Throwable failure) { + if (mantleReleased) { + return failure; + } + Throwable mantleFailure = runCleanup(null, engine.getMantle()::close); + if (mantleFailure != null) { + return appendFailure(failure, mantleFailure); + } + mantleReleased = true; + return failure; + } + + private Throwable releaseEngineDataForShutdown(Throwable failure) { + if (engineDataReleased) { + return failure; + } + Throwable dataFailure = runCleanup(null, engine.engineDataStore::releaseEngineData); + if (dataFailure != null) { + return appendFailure(failure, dataFailure); + } + engineDataReleased = true; + return failure; + } + + private Throwable releasePreservation(Throwable failure) { + if (preservationReleased) { + return failure; + } + Throwable preservationFailure = runCleanup(null, () -> { + PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); + if (registry != null) { + registry.dereference(); + } + }); + if (preservationFailure != null) { + return appendFailure(failure, preservationFailure); + } + preservationReleased = true; + return failure; + } + + static Throwable runCleanup(Throwable failure, Runnable cleanup) { + try { + cleanup.run(); + } catch (Throwable e) { + return appendFailure(failure, e); + } + return failure; + } + + static Throwable appendFailure(Throwable failure, Throwable additional) { + if (additional == null) { + return failure; + } + if (failure == null) { + return additional; + } + if (failure != additional) { + failure.addSuppressed(additional); + } + return failure; + } + + static RuntimeException propagate(Throwable throwable) { + if (throwable instanceof RuntimeException runtimeException) { + return runtimeException; + } + if (throwable instanceof Error error) { + throw error; + } + return new IllegalStateException(throwable); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java b/core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java new file mode 100644 index 000000000..0c7f25d50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java @@ -0,0 +1,75 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +/** + * Process-wide registry of live {@link IrisEngine} instances driven by one shared repeating task. + * The task is created on the first registration and cancelled once the last engine unregisters, so + * no ticking work runs when no engine is loaded. + */ +final class EngineTickRegistry { + private static final Object TICK_LOCK = new Object(); + private static final Set TICK_ENGINES = Collections.newSetFromMap(new IdentityHashMap<>()); + private static int sharedTickTask = -1; + + private EngineTickRegistry() { + } + + static void registerTicking(IrisEngine engine) { + synchronized (TICK_LOCK) { + TICK_ENGINES.add(engine); + if (sharedTickTask == -1) { + sharedTickTask = J.ar(EngineTickRegistry::tickEngines, 1); + } + } + } + + static void unregisterTicking(IrisEngine engine) { + synchronized (TICK_LOCK) { + TICK_ENGINES.remove(engine); + if (TICK_ENGINES.isEmpty() && sharedTickTask != -1) { + J.car(sharedTickTask); + sharedTickTask = -1; + } + } + } + + private static void tickEngines() { + List engines; + synchronized (TICK_LOCK) { + engines = List.copyOf(TICK_ENGINES); + } + for (IrisEngine engine : engines) { + try { + engine.tickRandomPlayer(); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + } +} 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 31e989ae3..bef584071 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -38,7 +38,8 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.DataProvider; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.interpolation.Interpolated; @@ -49,6 +50,7 @@ import lombok.Getter; import lombok.ToString; import java.io.File; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -56,16 +58,29 @@ import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache"}) -@ToString(exclude = {"data", "gridBoundsCache"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"}) +@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); + private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int HEIGHT_BOUNDS_GRID = 4; @Getter(AccessLevel.NONE) private final transient ThreadLocal gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new); + /** + * Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact + * iteration order the map produces. The per-column height paths walk these arrays instead of + * allocating map/set iterators, and the frozen order keeps the floating point accumulation order + * identical to the map iteration it replaces. Mutating {@link #generators} after construction is + * not reflected here. + */ + @Getter(AccessLevel.NONE) + private final transient IrisInterpolator[] frozenInterpolators; + @Getter(AccessLevel.NONE) + private final transient IrisGenerator[][] frozenGenerators; private RNG rng; private double fluidHeight; private IrisData data; @@ -142,6 +157,15 @@ public class IrisComplex implements DataProvider { region.getAllBiomes(this).forEach(this::registerGenerators); }); } + int interpolatorCount = generators.size(); + frozenInterpolators = new IrisInterpolator[interpolatorCount]; + frozenGenerators = new IrisGenerator[interpolatorCount][]; + int frozenIndex = 0; + for (Map.Entry> entry : generators.entrySet()) { + frozenInterpolators[frozenIndex] = entry.getKey(); + frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]); + frozenIndex++; + } generatorBounds = buildGeneratorBounds(engine); KList overlayNoise = engine.getDimension().getOverlayNoise(); overlayStream = overlayNoise.isEmpty() @@ -364,8 +388,8 @@ public class IrisComplex implements DataProvider { return biome; } - private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set generators, double x, double z, long seed) { - if (generators.isEmpty()) { + private double interpolateGenerators(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z, long seed) { + if (generators.length == 0) { return 0; } @@ -379,13 +403,14 @@ public class IrisComplex implements DataProvider { d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945)); } - return d / generators.size(); + return d / generators.length; } - private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set generators, double x, double z) { + private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) { int grid = HEIGHT_BOUNDS_GRID; + GridBoundsCache cache = gridBoundsCache.get(); if (grid <= 1) { - return sampleBoundsRaw(engine, interpolator, generators, x, z); + return sampleBoundsRaw(cache, engine, interpolator, generators, x, z); } int xi = (int) Math.floor(x); @@ -396,7 +421,6 @@ public class IrisComplex implements DataProvider { double fx = (x - gx) / grid; double fz = (z - gz) / grid; - GridBoundsCache cache = gridBoundsCache.get(); long b00 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz); long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz); long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid); @@ -407,13 +431,13 @@ public class IrisComplex implements DataProvider { return new NoiseBounds(lo, hi); } - private long cornerBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, Set generators, int gx, int gz) { + private long cornerBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, int gx, int gz) { int slot = cache.slot(gx, gz, interpolatorIndex); if (cache.valid[slot] && cache.gx[slot] == gx && cache.gz[slot] == gz && cache.idx[slot] == interpolatorIndex) { return cache.packed[slot]; } - NoiseBounds bounds = sampleBoundsRaw(engine, interpolator, generators, gx, gz); + NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz); long packed = (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL); cache.gx[slot] = gx; cache.gz[slot] = gz; @@ -437,37 +461,23 @@ public class IrisComplex implements DataProvider { return a + ((b - a) * fz); } - private NoiseBounds sampleBoundsRaw(Engine engine, IrisInterpolator interpolator, Set generators, double x, double z) { - CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64); + private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) { IdentityHashMap cachedBounds = generatorBounds.get(interpolator); - IdentityHashMap localBounds = new IdentityHashMap<>(8); - return interpolator.interpolateBounds(x, z, (xx, zz) -> { - try { - IrisBiome bx = sampleCache.get(xx, zz); - if (bx == null) { - bx = baseBiomeStream.get(xx, zz); - sampleCache.put(xx, zz, bx); - } + BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler; + sampler.bind(this, engine, generators, cachedBounds); - GeneratorBounds bounds = resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); - return bounds.noiseBounds; - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - IrisLogging.error("Failed to sample interpolated biome bounds at " + xx + " " + zz + "..."); - } - - return ZERO_NOISE_BOUNDS; - }); + try { + return interpolator.interpolateBounds(x, z, sampler); + } finally { + sampler.release(); + } } private double getInterpolatedHeight(Engine engine, double x, double z, long seed) { double h = 0; - int interpolatorIndex = 0; - for (Map.Entry> entry : generators.entrySet()) { - h += interpolateGenerators(engine, entry.getKey(), interpolatorIndex, entry.getValue(), x, z, seed); - interpolatorIndex++; + for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) { + h += interpolateGenerators(engine, frozenInterpolators[interpolatorIndex], interpolatorIndex, frozenGenerators[interpolatorIndex], x, z, seed); } return h; @@ -509,18 +519,18 @@ public class IrisComplex implements DataProvider { allBiomes.add(focusBiome); } - for (Map.Entry> entry : generators.entrySet()) { + for (int i = 0; i < frozenInterpolators.length; i++) { IdentityHashMap interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16)); for (IrisBiome biome : allBiomes) { - interpolatorBounds.put(biome, computeGeneratorBounds(engine, entry.getValue(), biome)); + interpolatorBounds.put(biome, computeGeneratorBounds(engine, frozenGenerators[i], biome)); } - bounds.put(entry.getKey(), interpolatorBounds); + bounds.put(frozenInterpolators[i], interpolatorBounds); } return bounds; } - private GeneratorBounds computeGeneratorBounds(Engine engine, Set generators, IrisBiome biome) { + private GeneratorBounds computeGeneratorBounds(Engine engine, IrisGenerator[] generators, IrisBiome biome) { double min = 0D; double max = 0D; @@ -539,7 +549,7 @@ public class IrisComplex implements DataProvider { private GeneratorBounds resolveGeneratorBounds( Engine engine, - Set generators, + IrisGenerator[] generators, IrisBiome biome, IdentityHashMap cachedBounds, IdentityHashMap localBounds @@ -627,6 +637,7 @@ public class IrisComplex implements DataProvider { private final int[] idx = new int[GRID_BOUNDS_CACHE_SIZE]; private final long[] packed = new long[GRID_BOUNDS_CACHE_SIZE]; private final boolean[] valid = new boolean[GRID_BOUNDS_CACHE_SIZE]; + private final BoundsSampler sampler = new BoundsSampler(); private int slot(int cornerX, int cornerZ, int interpolatorIndex) { long h = (cornerX * 0x9E3779B97F4A7C15L) ^ (cornerZ * 0xC2B2AE3D27D4EB4FL) ^ (interpolatorIndex * 0x165667B19E3779F9L); @@ -635,53 +646,193 @@ public class IrisComplex implements DataProvider { } } - private static class CoordinateBiomeCache { + /** + * Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator + * bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind} + * resets both, giving each pass the same empty-scratch semantics a fresh allocation had. + *

+ * Single threaded and non reentrant by contract, matching the thread local sample caches in + * IrisInterpolation that this provider is invoked through. If a nested pass ever does appear, + * {@link #isInUse()} makes the caller fall back to a freshly allocated sampler. + */ + private static final class BoundsSampler implements NoiseBoundsProvider { + private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64); + private final IdentityHashMap localBounds = new IdentityHashMap<>(8); + private IrisComplex complex; + private Engine engine; + private IrisGenerator[] generators; + private IdentityHashMap cachedBounds; + private boolean inUse; + + private boolean isInUse() { + return inUse; + } + + private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap cachedBounds) { + this.complex = complex; + this.engine = engine; + this.generators = generators; + this.cachedBounds = cachedBounds; + this.inUse = true; + sampleCache.clear(); + + if (!localBounds.isEmpty()) { + localBounds.clear(); + } + } + + private void release() { + complex = null; + engine = null; + generators = null; + cachedBounds = null; + inUse = false; + } + + @Override + public NoiseBounds noise(double xx, double zz) { + try { + IrisBiome bx = sampleCache.get(xx, zz); + if (bx == null) { + bx = complex.baseBiomeStream.get(xx, zz); + sampleCache.put(xx, zz, bx); + } + + GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); + return bounds.noiseBounds; + } catch (Throwable e) { + long now = System.currentTimeMillis(); + long last = lastBoundsFailureLog.get(); + if (now - last >= 5000L && lastBoundsFailureLog.compareAndSet(last, now)) { + IrisLogging.reportError(e); + IrisLogging.warn("Failed to sample interpolated biome bounds at " + xx + " " + zz + ", flattening height to zero: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + return ZERO_NOISE_BOUNDS; + } + } + + /** + * Open addressed biome memo keyed on the packed coordinate bits, replacing a linear scan that was + * quadratic in the number of columns a wide starcast touches. Single threaded by contract. + */ + private static final class CoordinateBiomeCache { private long[] xBits; private long[] zBits; private IrisBiome[] values; + private byte[] states; + private int mask; + private int resizeThreshold; private int size; - private CoordinateBiomeCache(int initialSize) { - xBits = new long[initialSize]; - zBits = new long[initialSize]; - values = new IrisBiome[initialSize]; + private CoordinateBiomeCache(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + values = new IrisBiome[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + private void clear() { + if (size == 0) { + return; + } + + Arrays.fill(states, (byte) 0); size = 0; } private IrisBiome get(double x, double z) { - long xb = Double.doubleToLongBits(x); - long zb = Double.doubleToLongBits(z); - for (int i = 0; i < size; i++) { - if (xBits[i] == xb && zBits[i] == zb) { - return values[i]; - } - } - - return null; + int slot = findSlot(Double.doubleToLongBits(x), Double.doubleToLongBits(z)); + return states[slot] == 0 ? null : values[slot]; } private void put(double x, double z, IrisBiome biome) { - if (size >= xBits.length) { - grow(); + long xb = Double.doubleToLongBits(x); + long zb = Double.doubleToLongBits(z); + int slot = findSlot(xb, zb); + boolean occupied = states[slot] != 0; + xBits[slot] = xb; + zBits[slot] = zb; + values[slot] = biome; + states[slot] = 1; + + if (occupied) { + return; } - xBits[size] = Double.doubleToLongBits(x); - zBits[size] = Double.doubleToLongBits(z); - values[size] = biome; size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int findSlot(long xb, long zb) { + int slot = mix(xb, zb) & mask; + while (states[slot] != 0) { + if (xBits[slot] == xb && zBits[slot] == zb) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private int mix(long xb, long zb) { + long hash = xb * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; } private void grow() { - int nextSize = xBits.length << 1; - long[] nx = new long[nextSize]; - long[] nz = new long[nextSize]; - IrisBiome[] nv = new IrisBiome[nextSize]; - System.arraycopy(xBits, 0, nx, 0, size); - System.arraycopy(zBits, 0, nz, 0, size); - System.arraycopy(values, 0, nv, 0, size); - xBits = nx; - zBits = nz; - values = nv; + long[] previousXBits = xBits; + long[] previousZBits = zBits; + IrisBiome[] previousValues = values; + byte[] previousStates = states; + + int nextLength = previousXBits.length << 1; + xBits = new long[nextLength]; + zBits = new long[nextLength]; + values = new IrisBiome[nextLength]; + states = new byte[nextLength]; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + values[slot] = previousValues[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int tableSize = n + 1; + if (tableSize < 8) { + return 8; + } + return tableSize; } } 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 ef5942699..76d49145e 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -18,45 +18,28 @@ package art.arcane.iris.engine; -import art.arcane.iris.core.localization.BukkitRuntimeMessages; -import art.arcane.iris.core.localization.ClientUiMessages; -import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain; +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineEffects; -import art.arcane.iris.engine.framework.EngineEffectsProvider; import art.arcane.iris.engine.framework.EngineMetrics; import art.arcane.iris.engine.framework.EngineMode; import art.arcane.iris.engine.framework.EnginePlatformHooks; import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.EngineWorldManager; -import art.arcane.iris.engine.framework.EngineWorldManagerProvider; import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionLease; import art.arcane.iris.engine.framework.GenerationSessionManager; -import art.arcane.iris.engine.framework.IrisStructureLocator; -import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.engine.framework.SeedManager; -import art.arcane.iris.engine.framework.StructureReachability; import art.arcane.iris.engine.framework.WrongEngineBroException; import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBiomePaletteLayer; -import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisDimensionMode; -import art.arcane.iris.engine.object.IrisDimensionModeType; import art.arcane.iris.engine.object.IrisEngineData; -import art.arcane.iris.engine.object.IrisObjectPlacement; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.spi.IrisLogging; import com.google.common.util.concurrent.AtomicDouble; -import com.google.gson.Gson; -import com.google.gson.JsonParseException; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisServices; -import art.arcane.iris.spi.protocol.IrisMessage; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.protocol.IrisProtocolServer; -import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.nms.container.BlockPos; import art.arcane.iris.core.nms.container.Pair; import art.arcane.iris.core.structure.StructureIndexService; @@ -66,56 +49,30 @@ import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.volmlib.util.atomics.AtomicRollingSequence; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.documentation.BlockCoordinates; -import art.arcane.iris.util.common.format.C; -import art.arcane.volmlib.util.format.Form; import art.arcane.iris.util.project.hunk.Hunk; -import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.mantle.flag.MantleFlag; -import art.arcane.volmlib.util.localization.MessageArgument; import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.scheduling.ChronoLatch; -import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.Setter; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @Data public class IrisEngine implements Engine { - private static final long SESSION_DRAIN_TIMEOUT_MILLIS = 15000L; - private static final long BACKGROUND_TASK_TIMEOUT_MILLIS = 15000L; - private static final Object TICK_LOCK = new Object(); - private static final Set TICK_ENGINES = Collections.newSetFromMap(new IdentityHashMap<>()); - private static int sharedTickTask = -1; + static final long SESSION_DRAIN_TIMEOUT_MILLIS = 15000L; private final AtomicInteger bud; private final AtomicInteger buds; @@ -132,19 +89,28 @@ public class IrisEngine implements Engine { private final AtomicRollingSequence wallClock; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private final Object lifecycleLock = new Object(); + final Object lifecycleLock = new Object(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private final Object backgroundTaskLock = new Object(); + final ThreadLocal runtimeAssembly = new ThreadLocal<>(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private final Object engineDataLock = new Object(); + final EngineBackgroundTasks backgroundTasks = new EngineBackgroundTasks(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private final List backgroundTasks = new ArrayList<>(); + final EngineDataStore engineDataStore = new EngineDataStore(this); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private final ThreadLocal runtimeAssembly = new ThreadLocal<>(); + final EngineRuntimeBuilder runtimeBuilder = new EngineRuntimeBuilder(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineShutdownSequence shutdownSequence = new EngineShutdownSequence(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineHotloader hotloader = new EngineHotloader(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final EngineMetricsReport metricsReport = new EngineMetricsReport(this); private final AtomicBoolean cleaning; private final ChronoLatch cleanLatch; private final SeedManager seedManager; @@ -152,25 +118,19 @@ public class IrisEngine implements Engine { private final EnginePlatformHooks platformHooks; private final AtomicBoolean closing; @Setter(AccessLevel.NONE) - private volatile IrisEngineData engineData; + volatile IrisEngineData engineData; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private volatile EngineRuntime runtime; + volatile EngineRuntime runtime; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private volatile EngineTarget publishedTarget; + volatile EngineTarget publishedTarget; private volatile int parallelism; private volatile boolean failing; - private volatile boolean closed; - private boolean runtimeReleased; - private boolean targetReleased; - private boolean mantleReleased; - private boolean engineDataReleased; - private boolean preservationReleased; + volatile boolean closed; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private volatile LifecycleState lifecycleState; - private boolean backgroundTaskAdmission; + volatile LifecycleState lifecycleState; private final AtomicBoolean modeFallbackLogged; private final AtomicBoolean prefetchSaveStarted; @@ -230,15 +190,15 @@ public class IrisEngine implements Engine { } IrisLogging.info("Engine init: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed()); _t0 = M.ms(); - EngineRuntime initialRuntime = buildRuntime(); - publishRuntime(initialRuntime, null); + EngineRuntime initialRuntime = runtimeBuilder.buildRuntime(); + runtimeBuilder.publishRuntime(initialRuntime, null); IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); _t0 = M.ms(); GenerationCacheWarmer.warm(this); IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms"); - registerTicking(this); + EngineTickRegistry.registerTicking(this); } catch (Throwable e) { - cleanupFailedConstruction(e); + shutdownSequence.cleanupFailedConstruction(e); throw new IllegalStateException("Failed to initialize Iris engine for world '" + target.getWorld().name() + "'.", e); } IrisLogging.debug("Engine Initialized " + getCacheID()); @@ -250,7 +210,7 @@ public class IrisEngine implements Engine { } } - private void tickRandomPlayer() { + void tickRandomPlayer() { if (closing.get() || closed) { return; } @@ -266,40 +226,6 @@ public class IrisEngine implements Engine { } } - private static void registerTicking(IrisEngine engine) { - synchronized (TICK_LOCK) { - TICK_ENGINES.add(engine); - if (sharedTickTask == -1) { - sharedTickTask = J.ar(IrisEngine::tickEngines, 1); - } - } - } - - private static void unregisterTicking(IrisEngine engine) { - synchronized (TICK_LOCK) { - TICK_ENGINES.remove(engine); - if (TICK_ENGINES.isEmpty() && sharedTickTask != -1) { - J.car(sharedTickTask); - sharedTickTask = -1; - } - } - } - - private static void tickEngines() { - List engines; - synchronized (TICK_LOCK) { - engines = List.copyOf(TICK_ENGINES); - } - for (IrisEngine engine : engines) { - try { - engine.tickRandomPlayer(); - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - } - private void sealInitialGenerationSession() { try { generationSessions.sealAndAwait("initialization", 0L); @@ -308,276 +234,18 @@ public class IrisEngine implements Engine { } } - private void sealForTransition(String reason, boolean teardown) { + void sealForTransition(String reason, boolean teardown) { closing.set(true); - closeBackgroundTaskAdmission(); + backgroundTasks.closeBackgroundTaskAdmission(); try { generationSessions.sealAndAwait(reason, SESSION_DRAIN_TIMEOUT_MILLIS, teardown); - BackgroundTaskDrain backgroundDrain = drainBackgroundTasks(reason); + BackgroundTaskDrain backgroundDrain = backgroundTasks.drainBackgroundTasks(reason); backgroundDrain.requireComplete(reason); } catch (GenerationSessionException e) { throw new IllegalStateException("Failed to drain Iris generation for " + reason + ".", e); } } - private EngineRuntime buildRuntime() { - return buildRuntime(getTarget()); - } - - private EngineRuntime buildRuntime(EngineTarget runtimeTarget) { - RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), runtimeTarget); - runtimeAssembly.set(assembly); - try (IrisContext.Scope ignored = IrisContext.open(this, generationSessions.currentSessionId(), null)) { - IrisLogging.debug("Setup Engine " + assembly.cacheId); - long started = M.ms(); - assembly.complex = new IrisComplex(this); - IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms"); - started = M.ms(); - assembly.upperContext = buildUpperContext(); - IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms"); - started = M.ms(); - assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(this); - if (assembly.effects == null) { - throw new IllegalStateException("Engine effects provider returned null"); - } - IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms"); - assembly.hash32 = new CompletableFuture<>(); - started = M.ms(); - mantle.hotload(); - IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms"); - started = M.ms(); - assembly.mode = createMode(); - IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms"); - started = M.ms(); - assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(this); - if (assembly.worldManager == null) { - throw new IllegalStateException("Engine world manager provider returned null"); - } - IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms"); - BiomeMaxes biomeMaxes = computeBiomeMaxes(); - return assembly.freeze(biomeMaxes); - } catch (Throwable e) { - Throwable cleanupFailure = closeAssembly(assembly, e); - if (cleanupFailure != e) { - e.addSuppressed(cleanupFailure); - } - throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e); - } finally { - runtimeAssembly.remove(); - } - } - - private void publishRuntime(EngineRuntime next, EngineRuntime previous) { - Throwable retirementFailure = closeRuntime(previous, null); - if (retirementFailure != null) { - retirementFailure = closeRuntime(next, retirementFailure); - lifecycleState = LifecycleState.FAILED; - throw new IllegalStateException("Failed to retire the previous Iris engine runtime.", retirementFailure); - } - if (runtime == previous) { - runtime = null; - } - - try { - next.worldManager.start(); - } catch (Throwable e) { - Throwable cleanupFailure = closeRuntime(next, e); - if (cleanupFailure != e) { - e.addSuppressed(cleanupFailure); - } - lifecycleState = LifecycleState.FAILED; - throw new IllegalStateException("Failed to start the Iris world manager.", e); - } - - runtime = next; - publishedTarget = next.target; - generationSessions.activateNextSession(); - lifecycleState = LifecycleState.RUNNING; - closing.set(false); - openBackgroundTaskAdmission(); - scheduleRuntimeTasks(next); - IrisLogging.debug("Engine Setup Complete " + next.cacheId); - } - - private void scheduleRuntimeTasks(EngineRuntime engineRuntime) { - try { - if (!scheduleTrackedTask(() -> { - try { - File[] roots = getData().getLoaders() - .values() - .stream() - .map(ResourceLoader::getFolderName) - .map(name -> new File(getData().getDataFolder(), name)) - .filter(File::exists) - .filter(File::isDirectory) - .toArray(File[]::new); - engineRuntime.hash32.complete(IO.hashRecursiveMeta(roots)); - } catch (Throwable e) { - engineRuntime.hash32.completeExceptionally(e); - throw propagate(e); - } - })) { - throw new IllegalStateException("Iris background task admission closed before pack hashing."); - } - } catch (Throwable e) { - engineRuntime.hash32.completeExceptionally(e); - IrisLogging.reportError(e); - e.printStackTrace(); - } - try { - if (!scheduleTrackedTask(() -> platformHooks.refreshDatapackWorkspace(this))) { - throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh."); - } - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - - private boolean scheduleTrackedTask(Runnable task) { - synchronized (backgroundTaskLock) { - backgroundTasks.removeIf(tracked -> tracked.completion.isDone() - && !tracked.completion.isCompletedExceptionally()); - if (!backgroundTaskAdmission) { - return false; - } - TrackedBackgroundTask tracked = new TrackedBackgroundTask(); - Future future = J.a(() -> { - tracked.started.set(true); - try { - task.run(); - tracked.completion.complete(null); - return null; - } catch (Throwable exception) { - tracked.completion.completeExceptionally(exception); - throw propagate(exception); - } - }); - if (future == null) { - throw new IllegalStateException("Iris background task scheduler returned no task handle."); - } - tracked.future = future; - backgroundTasks.add(tracked); - return true; - } - } - - private BackgroundTaskDrain drainBackgroundTasks(String reason) { - List tasks; - synchronized (backgroundTaskLock) { - tasks = List.copyOf(backgroundTasks); - } - long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS); - Throwable failure = null; - for (TrackedBackgroundTask task : tasks) { - long remaining = deadline - System.nanoTime(); - if (remaining <= 0L) { - cancelBackgroundTask(task, reason); - failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + ".")); - continue; - } - try { - task.completion.get(remaining, TimeUnit.NANOSECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - cancelBackgroundTask(task, reason); - failure = appendFailure(failure, e); - } catch (ExecutionException | TimeoutException e) { - cancelBackgroundTask(task, reason); - failure = appendFailure(failure, e); - } - } - boolean complete; - synchronized (backgroundTaskLock) { - backgroundTasks.removeIf(tracked -> tracked.completion.isDone()); - complete = backgroundTasks.isEmpty(); - } - return new BackgroundTaskDrain(failure, complete); - } - - private void cancelBackgroundTask(TrackedBackgroundTask task, String reason) { - Future future = task.future; - if (future != null && future.cancel(true) && !task.started.get()) { - task.completion.completeExceptionally( - new IllegalStateException("Iris background task was cancelled before starting during " + reason + ".")); - } - } - - private void openBackgroundTaskAdmission() { - synchronized (backgroundTaskLock) { - backgroundTaskAdmission = true; - } - } - - private void closeBackgroundTaskAdmission() { - synchronized (backgroundTaskLock) { - backgroundTaskAdmission = false; - } - } - - private void cancelBackgroundTasks(String reason) { - List tasks; - synchronized (backgroundTaskLock) { - tasks = List.copyOf(backgroundTasks); - } - for (TrackedBackgroundTask task : tasks) { - cancelBackgroundTask(task, reason); - } - } - - private UpperDimensionContext buildUpperContext() { - IrisDimension dim = getDimension(); - if (!dim.hasUpperDimension()) { - return null; - } - String upperKey = dim.getUpperDimension(); - IrisDimension upperDim = upperKey.equals(dim.getLoadKey()) - ? dim - : IrisData.loadAnyDimension(upperKey, getData()); - if (upperDim != null) { - UpperDimensionContext ctx = UpperDimensionContext.create(this, upperDim); - IrisLogging.info("Upper dimension enabled: " + upperKey - + (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)")); - return ctx; - } - IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain."); - return null; - } - - private EngineMode createMode() { - Throwable configuredFailure = null; - try { - IrisDimensionMode configuredMode = getDimension().getMode(); - if (configuredMode == null) { - configuredMode = new IrisDimensionMode(); - getDimension().setMode(configuredMode); - } - EngineMode configured = configuredMode.create(this); - if (configured == null) { - throw new IllegalStateException("Dimension mode factory returned null"); - } - return configured; - } catch (Throwable e) { - configuredFailure = e; - IrisLogging.reportError(e); - e.printStackTrace(); - if (modeFallbackLogged.compareAndSet(false, true)) { - IrisLogging.warn("Failed to initialize configured dimension mode for " + getDimension().getLoadKey() + ", falling back to OVERWORLD mode."); - } - } - - try { - EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(this); - if (fallback == null) { - throw new IllegalStateException("OVERWORLD mode factory returned null"); - } - return fallback; - } catch (Throwable fallbackFailure) { - fallbackFailure.addSuppressed(configuredFailure); - throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure); - } - } - @Override public void generateMatter(int x, int z, boolean multicore, ChunkContext context) { try (GenerationSessionLease lease = acquireGenerationLease("matter_generate"); @@ -622,191 +290,20 @@ public class IrisEngine implements Engine { @Override public void hotload() { - hotloadSilently(); - platformHooks.fireHotloadEvent(this); + hotloader.hotload(); } public void hotloadComplex() { - synchronized (lifecycleLock) { - requireRunning("rebuild the biome complex"); - lifecycleState = LifecycleState.HOTLOADING; - EngineRuntime previous = runtime; - IrisComplex nextComplex = null; - try { - sealForTransition("complex hotload", false); - RuntimeAssembly assembly = new RuntimeAssembly(RNG.r.nextInt(), previous.target); - runtimeAssembly.set(assembly); - EngineRuntime next; - try (IrisContext.Scope ignored = IrisContext.open(this, generationSessions.currentSessionId(), null)) { - assembly.complex = new IrisComplex(this); - nextComplex = assembly.complex; - assembly.upperContext = buildUpperContext(); - BiomeMaxes biomeMaxes = computeBiomeMaxes(); - next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes); - } finally { - runtimeAssembly.remove(); - } - Throwable retirementFailure = runCleanup(null, previous.complex::close); - if (retirementFailure != null) { - retirementFailure = runCleanup(retirementFailure, nextComplex::close); - nextComplex = null; - lifecycleState = LifecycleState.FAILED; - throw new IllegalStateException("Failed to retire the previous Iris biome complex.", retirementFailure); - } - runtime = next; - generationSessions.activateNextSession(); - lifecycleState = LifecycleState.RUNNING; - closing.set(false); - openBackgroundTaskAdmission(); - } catch (Throwable e) { - if (nextComplex != null && nextComplex != previous.complex) { - Throwable cleanupFailure = runCleanup(null, nextComplex::close); - if (cleanupFailure != null) { - e.addSuppressed(cleanupFailure); - } - } - if (lifecycleState != LifecycleState.FAILED) { - restoreRuntimeAfterFailedTransition(previous); - } - throw new IllegalStateException("Failed to rebuild the Iris biome complex.", e); - } - } + hotloader.hotloadComplex(); } public void hotloadSilently() { - synchronized (lifecycleLock) { - requireRunning("hotload"); - lifecycleState = LifecycleState.HOTLOADING; - EngineRuntime previousRuntime = runtime; - IrisDimension previousDimension = getDimension(); - IrisData previousData = getData(); - IrisData replacementData = null; - boolean published = false; - try { - sealForTransition("hotload", false); - replacementData = IrisData.openRuntime(previousData.getDataFolder()); - IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey()); - if (replacement == null) { - throw new IllegalStateException("Studio hotload could not reload Iris dimension '" + previousDimension.getLoadKey() + "'"); - } - platformHooks.validateDimensionHotload(this, replacement); - replacementData.registerEngine(this); - IrisStructureLocator.invalidate(this); - StructureReachability.invalidate(this); - EngineTarget replacementTarget = new EngineTarget(getWorld(), replacement, replacementData); - EngineRuntime nextRuntime = buildRuntime(replacementTarget); - publishRuntime(nextRuntime, previousRuntime); - published = true; - previousData.unregisterEngine(this); - Throwable previousDataFailure = runCleanup(null, previousData::close); - if (previousDataFailure != null) { - IrisLogging.error("Failed to completely release the previous Iris data runtime."); - IrisLogging.reportError(previousDataFailure); - previousDataFailure.printStackTrace(); - } - prefetchSaveStarted.set(false); - getEngineData().getStatistics().hotloaded(); - if (getWorld().hasPlatformWorld()) { - if (!scheduleTrackedTask(() -> { - platformHooks.refreshWorkspace(this); - platformHooks.reloadDatapacks(this); - })) { - throw new IllegalStateException("Iris background task admission closed before workspace refresh."); - } - } - broadcastStudioHotload(false, ""); - } catch (Throwable e) { - if (!published) { - if (replacementData != null) { - replacementData.unregisterEngine(this); - Throwable replacementDataFailure = runCleanup(null, replacementData::close); - if (replacementDataFailure != null) { - e.addSuppressed(replacementDataFailure); - } - } - IrisStructureLocator.invalidate(this); - StructureReachability.invalidate(this); - if (lifecycleState != LifecycleState.FAILED) { - Throwable rollbackFailure = runCleanup(null, getMantle()::hotload); - if (rollbackFailure == null) { - restoreRuntimeAfterFailedTransition(previousRuntime); - } else { - runtime = previousRuntime; - lifecycleState = LifecycleState.FAILED; - e.addSuppressed(rollbackFailure); - } - } - } - broadcastStudioHotload(true, e.getClass().getSimpleName() + ": " + e.getMessage()); - if (e instanceof Error error) { - throw error; - } - if (e instanceof RuntimeException runtimeException) { - throw runtimeException; - } - throw new IllegalStateException("Iris hotload failed.", e); - } - } - } - - private void broadcastStudioHotload(boolean failed, String message) { - IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class); - if (protocolServer == null) { - return; - } - IrisDimension dimension = getDimension(); - String packKey = dimension == null ? "" : dimension.getLoadKey(); - protocolServer.broadcastStudioHotload(packKey, 0, failed, message); - protocolServer.broadcastToast( - failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS, - IrisLanguage.plain(ClientUiMessages.TOAST_STUDIO_HOTLOAD), - failed ? IrisLanguage.plain(ClientUiMessages.TOAST_PACK_FAILED, MessageArgument.untrusted("pack", packKey)) : packKey); + hotloader.hotloadSilently(); } @Override public IrisEngineData getEngineData() { - IrisEngineData loaded = engineData; - if (loaded != null) { - return loaded; - } - synchronized (engineDataLock) { - loaded = engineData; - if (loaded != null) { - return loaded; - } - File f = new File(getWorld().worldFolder(), "iris/engine-data/" + getDimension().getLoadKey() + ".json"); - if (f.exists()) { - try { - loaded = new Gson().fromJson(IO.readAll(f), IrisEngineData.class); - if (loaded == null) { - throw new IllegalStateException("Engine data file contains no JSON object: " + f.getAbsolutePath()); - } - } catch (IOException | JsonParseException e) { - IrisLogging.reportError(e); - e.printStackTrace(); - throw new IllegalStateException("Failed to read Iris engine data without modifying it: " + f.getAbsolutePath(), e); - } - } - - if (loaded == null) { - loaded = new IrisEngineData(); - loaded.getStatistics().setVersion(IrisPlatforms.get().irisVersionNumber()); - loaded.getStatistics().setMCVersion(IrisPlatforms.get().minecraftVersionNumber()); - loaded.getStatistics().setUpgradedVersion(IrisPlatforms.get().irisVersionNumber()); - if (loaded.getStatistics().getVersion() == -1 || loaded.getStatistics().getMCVersion() == -1) { - IrisLogging.error("Failed to setup Engine Data!"); - } - try { - writeEngineDataAtomically(f, loaded); - } catch (IOException e) { - IrisLogging.reportError(e); - e.printStackTrace(); - throw new IllegalStateException("Failed to create Iris engine data: " + f.getAbsolutePath(), e); - } - } - engineData = loaded; - return loaded; - } + return engineDataStore.getEngineData(); } @Override @@ -816,20 +313,7 @@ public class IrisEngine implements Engine { @Override public double getGeneratedPerSecond() { - if (perSecondLatch.flip()) { - double g = generated.get() - generatedLast.get(); - generatedLast.set(generated.get()); - - if (g == 0) { - return 0; - } - - long dur = M.ms() - lastGPS.get(); - lastGPS.set(M.ms()); - perSecond.set(g / ((double) (dur) / 1000D)); - } - - return perSecond.get(); + return metricsReport.getGeneratedPerSecond(); } @Override @@ -837,173 +321,31 @@ public class IrisEngine implements Engine { return studio; } - private BiomeMaxes computeBiomeMaxes() { - double objectDensity = 0D; - double layerDensity = 0D; - double decoratorDensity = 0D; - for (IrisBiome i : getDimension().getReachableBiomes(this)) { - double density = 0; - - for (IrisObjectPlacement j : i.getObjects()) { - density += j.getDensity() * j.getChance(); - } - - objectDensity = Math.max(objectDensity, density); - density = 0; - - for (IrisDecorator j : i.getDecorators()) { - density += Math.max(j.getStackMax(), 1) * j.getChance(); - } - - decoratorDensity = Math.max(decoratorDensity, density); - density = 0; - - for (IrisBiomePaletteLayer j : i.getLayers()) { - density++; - } - - layerDensity = Math.max(layerDensity, density); - } - return new BiomeMaxes(objectDensity, layerDensity, decoratorDensity); - } - @Override public int getBlockUpdatesPerSecond() { return buds.get(); } public void printMetrics(VolmitSender sender) { - KMap totals = new KMap<>(); - KMap weights = new KMap<>(); - double masterWallClock = wallClock.getAverage(); - KMap timings = getMetrics().pull(); - double totalWeight = 0; - double wallClock = getMetrics().getTotal().getAverage(); - - for (double j : timings.values()) { - totalWeight += j; - } - - for (String j : timings.k()) { - weights.put(getName() + "." + j, (wallClock / totalWeight) * timings.get(j)); - } - - totals.put(getName(), wallClock); - - double mtotals = 0; - - for (double i : totals.values()) { - mtotals += i; - } - - for (String i : totals.k()) { - totals.put(i, (masterWallClock / mtotals) * totals.get(i)); - } - - double v = 0; - - for (double i : weights.values()) { - v += i; - } - - for (String i : weights.k()) { - weights.put(i, weights.get(i) / v); - } - - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_TOTAL, MessageArgument.untrusted("value", String.valueOf(Form.duration(masterWallClock, 0))))); - - for (String i : totals.k()) { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_ENGINE, MessageArgument.untrusted("i", String.valueOf(i)), MessageArgument.untrusted("value", String.valueOf(Form.duration(totals.get(i), 0))))); - } - - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_DETAILS)); - - for (String i : weights.sortKNumber().reverse()) { - String befb = C.UNDERLINE + "" + C.GREEN + "" + i.split("\\Q[\\E")[0] + C.RESET + C.GRAY + "["; - String num = C.GOLD + i.split("\\Q[\\E")[1].split("]")[0] + C.RESET + C.GRAY + "]."; - String afb = C.ITALIC + "" + C.AQUA + i.split("\\Q]\\E")[1].substring(1) + C.RESET + C.GRAY; - - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_ENGINE_MESSAGE, MessageArgument.untrusted("befb", String.valueOf(befb)), MessageArgument.untrusted("num", String.valueOf(num)), MessageArgument.untrusted("afb", String.valueOf(afb)), MessageArgument.untrusted("value", String.valueOf(Form.pc(weights.get(i), 0))))); - } + metricsReport.printMetrics(sender); } @Override public void close() { - Throwable failure; - synchronized (lifecycleLock) { - if (closed) { - return; - } - lifecycleState = LifecycleState.CLOSING; - closing.set(true); - closeBackgroundTaskAdmission(); - unregisterTicking(this); - platformHooks.shutdownPregenerator(this); - try { - generationSessions.sealAndAwait("close", SESSION_DRAIN_TIMEOUT_MILLIS, true); - } catch (GenerationSessionException e) { - throw new IllegalStateException("Failed to drain Iris generation for close.", e); - } - - BackgroundTaskDrain backgroundDrain = drainBackgroundTasks("close"); - failure = backgroundDrain.failure; - if (!backgroundDrain.allowsResourceRelease() && failure == null) { - failure = new IllegalStateException("Iris background tasks remain active during close."); - } - - if (backgroundDrain.allowsResourceRelease()) { - Throwable prefetchFailure = runCleanup(null, this::savePrefetchOnce); - Throwable engineDataFailure = runCleanup(null, this::saveEngineData); - failure = appendFailure(failure, prefetchFailure); - failure = appendFailure(failure, engineDataFailure); - failure = releaseRuntime(failure); - if (runtimeReleased) { - failure = releaseTarget(failure); - } - if (targetReleased) { - failure = releaseMantle(failure); - } - if (prefetchFailure == null - && engineDataFailure == null - && runtimeReleased - && targetReleased - && mantleReleased) { - failure = releaseEngineDataForShutdown(failure); - } - if (engineDataReleased) { - failure = releasePreservation(failure); - } - } - if (failure == null - && runtimeReleased - && targetReleased - && mantleReleased - && engineDataReleased - && preservationReleased) { - closed = true; - lifecycleState = LifecycleState.CLOSED; - IrisLogging.debug("Engine Fully Shutdown!"); - } - } - if (failure != null) { - IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + getWorld().name() + "."); - IrisLogging.reportError(failure); - failure.printStackTrace(); - throw new IllegalStateException("Iris engine shutdown remains incomplete after cleanup failures.", failure); - } + shutdownSequence.close(); } private boolean isPregeneratorActiveForThisWorld() { return platformHooks.isPregeneratorActive(this); } - private void savePrefetchOnce() { + void savePrefetchOnce() { if (prefetchSaveStarted.compareAndSet(false, true)) { try { getData().savePrefetch(this); } catch (Throwable e) { prefetchSaveStarted.set(false); - throw propagate(e); + throw EngineShutdownSequence.propagate(e); } } } @@ -1015,7 +357,7 @@ public class IrisEngine implements Engine { return assembly.complex; } EngineRuntime current = runtime; - return current == null ? null : current.complex; + return current == null ? null : current.complex(); } @Override @@ -1025,7 +367,7 @@ public class IrisEngine implements Engine { return assembly.target; } EngineRuntime current = runtime; - return current == null ? publishedTarget : current.target; + return current == null ? publishedTarget : current.target(); } @Override @@ -1034,8 +376,8 @@ public class IrisEngine implements Engine { if (assembly != null && assembly.mode != null) { return assembly.mode; } - EngineRuntime current = requireRuntime("access the engine mode"); - return current.mode; + EngineRuntime current = runtimeBuilder.requireRuntime("access the engine mode"); + return current.mode(); } @Override @@ -1045,7 +387,7 @@ public class IrisEngine implements Engine { return assembly.effects; } EngineRuntime current = runtime; - return current == null ? null : current.effects; + return current == null ? null : current.effects(); } @Override @@ -1055,7 +397,7 @@ public class IrisEngine implements Engine { return assembly.worldManager; } EngineRuntime current = runtime; - return current == null ? null : current.worldManager; + return current == null ? null : current.worldManager(); } @Override @@ -1065,7 +407,7 @@ public class IrisEngine implements Engine { return assembly.upperContext; } EngineRuntime current = runtime; - return current == null ? null : current.upperContext; + return current == null ? null : current.upperContext(); } @Override @@ -1074,26 +416,26 @@ public class IrisEngine implements Engine { if (assembly != null && assembly.hash32 != null) { return assembly.hash32; } - EngineRuntime current = requireRuntime("access the pack hash"); - return current.hash32; + EngineRuntime current = runtimeBuilder.requireRuntime("access the pack hash"); + return current.hash32(); } @Override public double getMaxBiomeObjectDensity() { EngineRuntime current = runtime; - return current == null ? 0D : current.biomeMaxes.objectDensity; + return current == null ? 0D : current.biomeMaxes().objectDensity(); } @Override public double getMaxBiomeLayerDensity() { EngineRuntime current = runtime; - return current == null ? 0D : current.biomeMaxes.layerDensity; + return current == null ? 0D : current.biomeMaxes().layerDensity(); } @Override public double getMaxBiomeDecoratorDensity() { EngineRuntime current = runtime; - return current == null ? 0D : current.biomeMaxes.decoratorDensity; + return current == null ? 0D : current.biomeMaxes().decoratorDensity(); } @Override @@ -1121,7 +463,7 @@ public class IrisEngine implements Engine { cleaning.set(true); - if (!scheduleTrackedTask(() -> { + if (!backgroundTasks.scheduleTrackedTask(() -> { try { getData().getObjectLoader().clean(); } catch (Throwable e) { @@ -1164,7 +506,7 @@ public class IrisEngine implements Engine { generated.incrementAndGet(); if (generated.get() == 661 && !isPregeneratorActiveForThisWorld()) { - scheduleTrackedTask(this::savePrefetchOnce); + backgroundTasks.scheduleTrackedTask(this::savePrefetchOnce); } } catch (GenerationSessionException e) { throw e; @@ -1185,18 +527,7 @@ public class IrisEngine implements Engine { @Override public void saveEngineData() { - synchronized (engineDataLock) { - File f = new File(getWorld().worldFolder(), "iris/engine-data/" + getDimension().getLoadKey() + ".json"); - try { - writeEngineDataAtomically(f, getEngineData()); - IrisLogging.debug("Saved Engine Data"); - } catch (IOException e) { - IrisLogging.error("Failed to save Engine Data"); - IrisLogging.reportError(e); - e.printStackTrace(); - throw new IllegalStateException("Failed to save Iris engine data: " + f.getAbsolutePath(), e); - } - } + engineDataStore.saveEngineData(); } @Override @@ -1242,247 +573,16 @@ public class IrisEngine implements Engine { return assembly.cacheId; } EngineRuntime current = runtime; - return current == null ? -1 : current.cacheId; + return current == null ? -1 : current.cacheId(); } - private void requireRunning(String operation) { + void requireRunning(String operation) { if (closed || closing.get() || lifecycleState != LifecycleState.RUNNING || runtime == null) { throw new IllegalStateException("Cannot " + operation + " while Iris engine " + getWorld().name() + " is " + lifecycleState.name().toLowerCase() + "."); } } - private void restoreRuntimeAfterFailedTransition(EngineRuntime previous) { - runtime = previous; - if (generationSessions.activeLeases() == 0) { - generationSessions.activateNextSession(); - lifecycleState = LifecycleState.RUNNING; - closing.set(false); - openBackgroundTaskAdmission(); - return; - } - lifecycleState = LifecycleState.FAILED; - } - - private EngineRuntime requireRuntime(String operation) { - EngineRuntime current = runtime; - if (current == null) { - throw new IllegalStateException("Cannot " + operation + " without an active Iris runtime for " - + getWorld().name() + "."); - } - return current; - } - - private void releaseEngineData() { - IrisData data = getData(); - data.unregisterEngine(this); - if (data.getEngines().isEmpty()) { - data.close(); - data.clearLists(); - } - } - - private void cleanupFailedConstruction(Throwable original) { - closing.set(true); - closeBackgroundTaskAdmission(); - unregisterTicking(this); - lifecycleState = LifecycleState.FAILED; - Throwable cleanupFailure = null; - try { - generationSessions.sealAndAwait("failed initialization", 0L, true); - } catch (Throwable e) { - cleanupFailure = appendFailure(cleanupFailure, e); - } - cancelBackgroundTasks("failed initialization"); - BackgroundTaskDrain backgroundDrain = drainBackgroundTasks("failed initialization"); - cleanupFailure = appendFailure(cleanupFailure, backgroundDrain.failure); - if (!backgroundDrain.allowsResourceRelease()) { - cleanupFailure = appendFailure(cleanupFailure, - new IllegalStateException("Iris background tasks remain active after failed initialization.")); - if (cleanupFailure != original) { - original.addSuppressed(cleanupFailure); - } - return; - } - cleanupFailure = closeRuntime(runtime, cleanupFailure); - runtime = null; - cleanupFailure = runCleanup(cleanupFailure, getTarget()::close); - cleanupFailure = runCleanup(cleanupFailure, getMantle()::close); - cleanupFailure = runCleanup(cleanupFailure, this::releaseEngineData); - closed = true; - cleanupFailure = runCleanup(cleanupFailure, () -> { - PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); - if (registry != null) { - registry.dereference(); - } - }); - if (cleanupFailure != null && cleanupFailure != original) { - original.addSuppressed(cleanupFailure); - } - } - - private Throwable closeAssembly(RuntimeAssembly assembly, Throwable failure) { - if (assembly == null) { - return failure; - } - failure = runCleanup(failure, () -> { - if (assembly.worldManager != null) { - assembly.worldManager.close(); - } - }); - failure = runCleanup(failure, () -> { - if (assembly.effects != null) { - assembly.effects.close(); - } - }); - failure = runCleanup(failure, () -> { - if (assembly.mode != null) { - assembly.mode.close(); - } - }); - failure = runCleanup(failure, () -> { - if (assembly.complex != null) { - assembly.complex.close(); - } - }); - failure = runCleanup(failure, () -> { - if (assembly.hash32 != null) { - assembly.hash32.cancel(true); - } - }); - return failure; - } - - private Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) { - if (engineRuntime == null) { - return failure; - } - failure = runCleanup(failure, engineRuntime.worldManager::close); - failure = runCleanup(failure, engineRuntime.effects::close); - failure = runCleanup(failure, engineRuntime.mode::close); - failure = runCleanup(failure, engineRuntime.complex::close); - failure = runCleanup(failure, () -> engineRuntime.hash32.cancel(true)); - return failure; - } - - private Throwable releaseRuntime(Throwable failure) { - if (runtimeReleased) { - return failure; - } - Throwable runtimeFailure = closeRuntime(runtime, null); - if (runtimeFailure != null) { - return appendFailure(failure, runtimeFailure); - } - runtime = null; - runtimeReleased = true; - return failure; - } - - private Throwable releaseTarget(Throwable failure) { - if (targetReleased) { - return failure; - } - Throwable targetFailure = runCleanup(null, getTarget()::close); - if (targetFailure != null) { - return appendFailure(failure, targetFailure); - } - targetReleased = true; - return failure; - } - - private Throwable releaseMantle(Throwable failure) { - if (mantleReleased) { - return failure; - } - Throwable mantleFailure = runCleanup(null, getMantle()::close); - if (mantleFailure != null) { - return appendFailure(failure, mantleFailure); - } - mantleReleased = true; - return failure; - } - - private Throwable releaseEngineDataForShutdown(Throwable failure) { - if (engineDataReleased) { - return failure; - } - Throwable dataFailure = runCleanup(null, this::releaseEngineData); - if (dataFailure != null) { - return appendFailure(failure, dataFailure); - } - engineDataReleased = true; - return failure; - } - - private Throwable releasePreservation(Throwable failure) { - if (preservationReleased) { - return failure; - } - Throwable preservationFailure = runCleanup(null, () -> { - PreservationRegistry registry = IrisServices.getOrNull(PreservationRegistry.class); - if (registry != null) { - registry.dereference(); - } - }); - if (preservationFailure != null) { - return appendFailure(failure, preservationFailure); - } - preservationReleased = true; - return failure; - } - - private static Throwable runCleanup(Throwable failure, Runnable cleanup) { - try { - cleanup.run(); - } catch (Throwable e) { - return appendFailure(failure, e); - } - return failure; - } - - private static Throwable appendFailure(Throwable failure, Throwable additional) { - if (additional == null) { - return failure; - } - if (failure == null) { - return additional; - } - if (failure != additional) { - failure.addSuppressed(additional); - } - return failure; - } - - private static RuntimeException propagate(Throwable throwable) { - if (throwable instanceof RuntimeException runtimeException) { - return runtimeException; - } - if (throwable instanceof Error error) { - throw error; - } - return new IllegalStateException(throwable); - } - - static void writeEngineDataAtomically(File file, IrisEngineData data) throws IOException { - Path output = file.toPath(); - Path parent = output.getParent(); - if (parent == null) { - throw new IOException("Engine data path has no parent: " + output); - } - Files.createDirectories(parent); - Path temporary = Files.createTempFile(parent, output.getFileName().toString(), ".tmp"); - try { - Files.writeString(temporary, new Gson().toJson(data), StandardCharsets.UTF_8); - try { - Files.move(temporary, output, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException e) { - Files.move(temporary, output, StandardCopyOption.REPLACE_EXISTING); - } - } finally { - Files.deleteIfExists(temporary); - } - } - private boolean EngineSafe() { // Todo: this has potential if done right int EngineMCVersion = getEngineData().getStatistics().getMCVersion(); @@ -1498,7 +598,7 @@ public class IrisEngine implements Engine { return true; } - private enum LifecycleState { + enum LifecycleState { INITIALIZING, RUNNING, HOTLOADING, @@ -1507,90 +607,4 @@ public class IrisEngine implements Engine { FAILED } - private record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) { - } - - private record EngineRuntime( - int cacheId, - EngineTarget target, - IrisComplex complex, - UpperDimensionContext upperContext, - EngineEffects effects, - EngineMode mode, - EngineWorldManager worldManager, - CompletableFuture hash32, - BiomeMaxes biomeMaxes - ) { - private EngineRuntime withComplex( - int nextCacheId, - IrisComplex nextComplex, - UpperDimensionContext nextUpperContext, - BiomeMaxes nextBiomeMaxes - ) { - return new EngineRuntime( - nextCacheId, - target, - nextComplex, - nextUpperContext, - effects, - mode, - worldManager, - hash32, - nextBiomeMaxes); - } - } - - private static final class RuntimeAssembly { - private final int cacheId; - private final EngineTarget target; - private IrisComplex complex; - private UpperDimensionContext upperContext; - private EngineEffects effects; - private EngineMode mode; - private EngineWorldManager worldManager; - private CompletableFuture hash32; - - private RuntimeAssembly(int cacheId, EngineTarget target) { - this.cacheId = cacheId; - this.target = target; - } - - private EngineRuntime freeze(BiomeMaxes biomeMaxes) { - if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) { - throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime."); - } - return new EngineRuntime( - cacheId, - target, - complex, - upperContext, - effects, - mode, - worldManager, - hash32, - biomeMaxes); - } - } - - private static final class TrackedBackgroundTask { - private final AtomicBoolean started = new AtomicBoolean(); - private final CompletableFuture completion = new CompletableFuture<>(); - private volatile Future future; - } - - record BackgroundTaskDrain(Throwable failure, boolean complete) { - boolean allowsResourceRelease() { - return complete; - } - - private void requireComplete(String reason) { - if (failure != null) { - throw new IllegalStateException("Iris background tasks failed to drain during " + reason + ".", failure); - } - if (!complete) { - throw new IllegalStateException("Iris background tasks remain active during " + reason + "."); - } - } - } - } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index b79adf5a3..c5d9d752d 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -18,116 +18,74 @@ package art.arcane.iris.engine; -import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.platform.bukkit.BukkitWorldBinding; import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.core.gui.PregeneratorJob; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.service.tree.BlockDropRouter; -import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedWorldManager; -import art.arcane.iris.engine.platform.EngineBukkitOps; -import art.arcane.iris.engine.object.IRare; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBlockDrops; -import art.arcane.iris.engine.object.IrisEntitySpawn; -import art.arcane.iris.engine.object.IrisMarker; -import art.arcane.iris.engine.object.IrisPosition; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisSpawner; import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; -import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.mantle.runtime.Mantle; -import art.arcane.volmlib.util.mantle.flag.MantleFlag; -import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; -import art.arcane.volmlib.util.matter.MatterMarker; -import art.arcane.iris.util.common.parallel.MultiBurst; -import art.arcane.iris.util.common.plugin.Chunks; -import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.Looper; -import art.arcane.iris.util.common.scheduling.jobs.QueueJob; -import io.papermc.lib.PaperLib; +import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.inventory.ItemStack; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; @EqualsAndHashCode(callSuper = true) @Data public class IrisWorldManager extends EngineAssignedWorldManager { - private static final int MAX_FORCED_CHUNK_UPDATES = 128; + private static final long CLOSE_AWAIT_MS = 3_000; private final Looper looper; private final KList updateQueue = new KList<>(); - private final ChronoLatch cl; + final ChronoLatch cl; private final ChronoLatch clw; private final ChronoLatch cln; private final ChronoLatch chunkUpdater; private final ChronoLatch chunkDiscovery; private final KMap> cleanup = new KMap<>(); private final ScheduledExecutorService cleanupService; - private final Set mantleWarmupQueue = ConcurrentHashMap.newKeySet(); - private final Set markerFlagQueue = ConcurrentHashMap.newKeySet(); - private final Set discoveredFlagQueue = ConcurrentHashMap.newKeySet(); - private final Set markerScanQueue = ConcurrentHashMap.newKeySet(); - private final Set chunkUpdateQueue = ConcurrentHashMap.newKeySet(); - private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean(); - private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean(); - private final AtomicBoolean entityCountWarningReported = new AtomicBoolean(); - private final AtomicBoolean entityCountErrorReported = new AtomicBoolean(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldEntitySpawner entitySpawner = new WorldEntitySpawner(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldChunkMaintenance chunkMaintenance = new WorldChunkMaintenance(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final MarkerSpawnScanner markerScanner = new MarkerSpawnScanner(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldBlockDropRouter blockDropRouter = new WorldBlockDropRouter(this); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + final WorldTeleportWarmup teleportWarmup = new WorldTeleportWarmup(this); private boolean looperStopped; private boolean cleanupServiceStopped; - private volatile int entityCount = 0; - private final AtomicInteger actuallySpawned = new AtomicInteger(); - private int cooldown = 0; - private int forcedChunkUpdateCursor = 0; - private volatile boolean entityCountValid = false; - private volatile boolean playersPresent = false; + volatile int entityCount = 0; + volatile boolean entityCountValid = false; + volatile boolean playersPresent = false; private KSet injectBiomes = new KSet<>(); - private volatile int loadedChunkCount = 0; + volatile int loadedChunkCount = 0; public IrisWorldManager() { super(null); @@ -182,7 +140,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { if (getEngine().getWorld().hasPlatformWorld()) { if (chunkUpdater.flip()) { - updateChunks(); + chunkMaintenance.updateChunks(); } if (!playersPresent) { @@ -190,7 +148,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } if (chunkDiscovery.flip()) { - discoverChunks(); + chunkMaintenance.discoverChunks(); } if (cln.flip()) { @@ -202,7 +160,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return 3000L; } - onAsyncTick(); + entitySpawner.onAsyncTick(); } return IrisSettings.get().getWorld().getAsyncTickIntervalMS(); @@ -216,11 +174,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } } - private Runnable managedTask(String operation, Runnable task) { + Runnable managedTask(String operation, Runnable task) { return () -> runManagerTask(operation, task); } - private Runnable managedTask(String operation, Runnable task, Runnable unavailable) { + Runnable managedTask(String operation, Runnable task, Runnable unavailable) { return () -> { if (!runManagerTask(operation, task)) { unavailable.run(); @@ -228,773 +186,8 @@ public class IrisWorldManager extends EngineAssignedWorldManager { }; } - private void discoverChunks() { - World world = BukkitWorldBinding.world(getEngine().getWorld()); - if (world == null) { - return; - } - - if (isPregenActiveForThisWorld()) { - return; - } - - if (!chunkDiscoveryScanScheduled.compareAndSet(false, true)) { - return; - } - - boolean scheduled = J.runGlobal(managedTask("bukkit_world_manager_discover_chunks", () -> { - try { - if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) { - return; - } - - for (Player player : world.getPlayers()) { - if (player == null) { - continue; - } - - J.runEntity(player, managedTask("bukkit_world_manager_discover_player", () -> { - if (!player.isOnline() || !world.equals(player.getWorld())) { - return; - } - - int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX()); - int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ()); - int radius = 1; - for (int x = -radius; x <= radius; x++) { - for (int z = -radius; z <= radius; z++) { - int chunkX = centerX + x; - int chunkZ = centerZ + z; - raiseDiscoveredChunkFlag(world, chunkX, chunkZ); - } - } - })); - } - } catch (Throwable e) { - IrisLogging.reportError(e); - } finally { - chunkDiscoveryScanScheduled.set(false); - } - }, () -> chunkDiscoveryScanScheduled.set(false))); - if (!scheduled) { - chunkDiscoveryScanScheduled.set(false); - } - } - - private void raiseDiscoveredChunkFlag(World world, int chunkX, int chunkZ) { - if (world == null) { - return; - } - - if (!J.isFolia()) { - getMantle().getChunk(chunkX, chunkZ).flag(MantleFlag.DISCOVERED, true); - return; - } - - long key = Cache.key(chunkX, chunkZ); - if (!discoveredFlagQueue.add(key)) { - return; - } - - J.a(managedTask("bukkit_world_manager_discovered_flag", () -> { - try { - Mantle mantle = getMantle(); - if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) { - mantle.flag(chunkX, chunkZ, MantleFlag.DISCOVERED, true); - } - } catch (Throwable e) { - IrisLogging.reportError(e); - } finally { - discoveredFlagQueue.remove(key); - } - }, () -> discoveredFlagQueue.remove(key))); - } - - private void updateChunks() { - World world = BukkitWorldBinding.world(getEngine().getWorld()); - if (world == null) { - return; - } - - if (isPregenActiveForThisWorld()) { - return; - } - - if (!chunkUpdateScanScheduled.compareAndSet(false, true)) { - return; - } - - boolean scheduled = J.runGlobal(managedTask( - "bukkit_world_manager_update_chunks", - () -> updateChunksOnGlobal(world), - () -> chunkUpdateScanScheduled.set(false))); - if (!scheduled) { - chunkUpdateScanScheduled.set(false); - } - } - - private void updateChunksOnGlobal(World world) { - try { - if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) { - return; - } - - List players = new ArrayList<>(world.getPlayers()); - playersPresent = !players.isEmpty(); - loadedChunkCount = world.getLoadedChunks().length; - for (Player player : players) { - if (player == null) { - continue; - } - - J.runEntity(player, managedTask( - "bukkit_world_manager_player_chunk_updates", - () -> schedulePlayerChunkUpdates(world, player))); - } - - scheduleForcedChunkUpdates(world); - } catch (Throwable e) { - IrisLogging.reportError(e); - } finally { - chunkUpdateScanScheduled.set(false); - } - } - - private void schedulePlayerChunkUpdates(World world, Player player) { - if (!player.isOnline() || !world.equals(player.getWorld())) { - return; - } - - int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX()); - int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ()); - int radius = 1; - for (int x = -radius; x <= radius; x++) { - for (int z = -radius; z <= radius; z++) { - scheduleChunkUpdate(world, centerX + x, centerZ + z); - } - } - } - - private void scheduleForcedChunkUpdates(World world) { - List forcedChunks = new ArrayList<>(); - for (Chunk chunk : world.getForceLoadedChunks()) { - forcedChunks.add(new Position2(chunk.getX(), chunk.getZ())); - } - forcedChunks.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ)); - - int forcedChunkCount = forcedChunks.size(); - if (forcedChunkCount == 0) { - forcedChunkUpdateCursor = 0; - return; - } - - int updateCount = Math.min(forcedChunkCount, MAX_FORCED_CHUNK_UPDATES); - int start = Math.floorMod(forcedChunkUpdateCursor, forcedChunkCount); - for (int i = 0; i < updateCount; i++) { - Position2 chunk = forcedChunks.get((start + i) % forcedChunkCount); - scheduleChunkUpdate(world, chunk.getX(), chunk.getZ()); - } - forcedChunkUpdateCursor = (start + updateCount) % forcedChunkCount; - } - - private void scheduleChunkUpdate(World world, int chunkX, int chunkZ) { - long key = Cache.key(chunkX, chunkZ); - if (!chunkUpdateQueue.add(key)) { - return; - } - - try { - boolean scheduled = J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_chunk_update", () -> { - try { - updateChunkRegion(world, chunkX, chunkZ); - } finally { - chunkUpdateQueue.remove(key); - } - }, () -> chunkUpdateQueue.remove(key))); - if (!scheduled) { - chunkUpdateQueue.remove(key); - } - } catch (Throwable e) { - chunkUpdateQueue.remove(key); - IrisLogging.reportError(e); - } - } - - private void updateChunkRegion(World world, int chunkX, int chunkZ) { - if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { - return; - } - - Chunk chunk = world.getChunkAt(chunkX, chunkZ); - - if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { - if (!getMantle().isChunkLoaded(chunkX, chunkZ)) { - warmupMantleChunkAsync(chunkX, chunkZ); - return; - } - EngineBukkitOps.updateChunk(getEngine(), chunk); - } - - if (!isEntitySpawningEnabledForCurrentWorld()) { - return; - } - - if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { - return; - } - - if (!J.isFolia() && !getMantle().isChunkLoaded(chunkX, chunkZ)) { - warmupMantleChunkAsync(chunkX, chunkZ); - return; - } - - raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> { - int delay = RNG.r.i(5, 200); - J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_initial_spawn_followup", () -> { - if (!world.isChunkLoaded(chunkX, chunkZ)) { - return; - } - spawnIn(world.getChunkAt(chunkX, chunkZ), true); - }), delay); - - Chunk markerChunk = world.getChunkAt(chunkX, chunkZ); - forEachMarkerSpawner(markerChunk, (block, spawners) -> { - IrisSpawner s = new KList<>(spawners).getRandom(); - if (s == null) { - return; - } - spawn(block, s, true); - }); - }); - } - - private void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) { - if (world == null || onFirstRaise == null) { - return; - } - - if (!J.isFolia()) { - getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise); - return; - } - - long key = Cache.key(chunkX, chunkZ); - if (!markerFlagQueue.add(key)) { - return; - } - - J.a(managedTask("bukkit_world_manager_spawn_marker_flag", () -> { - boolean raised = false; - try { - Mantle mantle = getMantle(); - if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) { - mantle.flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true); - raised = true; - } - } catch (Throwable e) { - IrisLogging.reportError(e); - } finally { - markerFlagQueue.remove(key); - } - - if (!raised) { - return; - } - - J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_spawn_marker_callback", () -> { - if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { - return; - } - onFirstRaise.run(); - })); - }, () -> markerFlagQueue.remove(key))); - } - - private void warmupMantleChunkAsync(int chunkX, int chunkZ) { - long key = Cache.key(chunkX, chunkZ); - if (!mantleWarmupQueue.add(key)) { - return; - } - - J.a(managedTask("bukkit_world_manager_mantle_warmup", () -> { - try { - getMantle().getChunk(chunkX, chunkZ); - } catch (Throwable e) { - IrisLogging.reportError(e); - } finally { - mantleWarmupQueue.remove(key); - } - }, () -> mantleWarmupQueue.remove(key))); - } - - private boolean onAsyncTick() { - if (getEngine().isClosing() || getEngine().isClosed()) { - return false; - } - - if (isPregenActiveForThisWorld()) { - J.sleep(500); - return false; - } - - actuallySpawned.set(0); - - if (!getEngine().getWorld().hasPlatformWorld()) { - IrisLogging.debug("Can't spawn. No real world"); - J.sleep(5000); - return false; - } - - if (cl.flip()) { - try { - World realWorld = BukkitWorldBinding.world(getEngine().getWorld()); - if (realWorld == null) { - entityCount = 0; - entityCountValid = false; - } else if (J.isFolia()) { - Integer count = getFoliaLivingEntityCount(realWorld); - if (count != null) { - entityCount = count; - entityCountValid = true; - resetEntityCountFailures(); - } else { - entityCountValid = false; - } - } else { - CompletableFuture future = new CompletableFuture<>(); - boolean scheduled = J.runGlobal(() -> { - try { - int count = 0; - for (Entity entity : realWorld.getEntities()) { - if (entity instanceof LivingEntity && !entity.isDead()) { - count++; - } - } - future.complete(count); - } catch (Throwable ex) { - future.completeExceptionally(ex); - } - }); - if (scheduled) { - entityCount = future.get(2, TimeUnit.SECONDS); - entityCountValid = true; - resetEntityCountFailures(); - } else { - reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null); - } - } - } catch (InterruptedException e) { - entityCountValid = false; - Thread.currentThread().interrupt(); - return false; - } catch (TimeoutException e) { - reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null); - } catch (ExecutionException e) { - Throwable cause = e.getCause() == null ? e : e.getCause(); - reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause); - } catch (Throwable e) { - reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e); - } - } - - if (!entityCountValid) { - return false; - } - - double epx = getEntitySaturation(); - if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) { - IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")"); - J.sleep(5000); - return false; - } - - int spawnBuffer = RNG.r.i(2, 12); - World world = BukkitWorldBinding.world(getEngine().getWorld()); - if (world == null) { - return false; - } - - Position2[] cc = getLoadedChunkPositionsSnapshot(world); - while (spawnBuffer-- > 0) { - if (getEngine().isClosing() || getEngine().isClosed()) { - return actuallySpawned.get() > 0; - } - - if (cc.length == 0) { - IrisLogging.debug("Can't spawn. No chunks!"); - return false; - } - - Position2 c = cc[RNG.r.nextInt(cc.length)]; - if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) { - return actuallySpawned.get() > 0; - } - } - - return actuallySpawned.get() > 0; - } - - private boolean isPregenActiveForThisWorld() { - World world = BukkitWorldBinding.world(getEngine().getWorld()); - if (world == null) { - return false; - } - - if (IrisToolbelt.isWorldMaintenanceActive(world)) { - return true; - } - - PregeneratorJob job = PregeneratorJob.getInstance(); - if (job == null) { - return false; - } - - return job.targetsWorldIdentity(WorldIdentity.serialize(world)); - } - - private Position2[] getLoadedChunkPositionsSnapshot(World world) { - if (world == null) { - return new Position2[0]; - } - - CompletableFuture future = new CompletableFuture<>(); - boolean scheduled = J.runGlobal(() -> { - try { - Chunk[] chunks = world.getLoadedChunks(); - Position2[] positions = new Position2[chunks.length]; - for (int i = 0; i < chunks.length; i++) { - positions[i] = new Position2(chunks[i].getX(), chunks[i].getZ()); - } - loadedChunkCount = positions.length; - future.complete(positions); - } catch (Throwable e) { - future.completeExceptionally(e); - } - }); - if (!scheduled) { - return new Position2[0]; - } - - try { - return future.get(2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return new Position2[0]; - } catch (ExecutionException | TimeoutException e) { - IrisLogging.reportError(e); - return new Position2[0]; - } - } - - private Integer getFoliaLivingEntityCount(World world) { - CompletableFuture> playerFuture = new CompletableFuture<>(); - boolean scheduled = J.runGlobal(() -> { - try { - playerFuture.complete(new ArrayList<>(world.getPlayers())); - } catch (Throwable e) { - playerFuture.completeExceptionally(e); - } - }); - if (!scheduled) { - reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null); - return null; - } - - List players; - try { - players = playerFuture.get(2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - entityCountValid = false; - Thread.currentThread().interrupt(); - return null; - } catch (TimeoutException e) { - reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null); - return null; - } catch (ExecutionException e) { - Throwable cause = e.getCause() == null ? e : e.getCause(); - reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause); - return null; - } - - Map candidates = new ConcurrentHashMap<>(); - AtomicBoolean incomplete = new AtomicBoolean(); - AtomicReference failure = new AtomicReference<>(); - - CountDownLatch latch = new CountDownLatch(players.size()); - for (Player player : players) { - if (player == null) { - latch.countDown(); - continue; - } - - if (!J.runEntity(player, () -> { - try { - if (!player.isOnline() || !world.equals(player.getWorld())) { - return; - } - candidates.put(player.getUniqueId().toString(), player); - for (Entity nearby : player.getNearbyEntities(64, 64, 64)) { - if (nearby != null) { - candidates.put(nearby.getUniqueId().toString(), nearby); - } - } - } catch (Throwable e) { - incomplete.set(true); - failure.compareAndSet(null, e); - } finally { - latch.countDown(); - } - })) { - incomplete.set(true); - latch.countDown(); - } - } - - if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) { - if (!Thread.currentThread().isInterrupted()) { - reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get()); - } - return null; - } - - AtomicInteger count = new AtomicInteger(); - incomplete.set(false); - failure.set(null); - CountDownLatch entityLatch = new CountDownLatch(candidates.size()); - for (Entity entity : candidates.values()) { - if (!J.runEntity(entity, () -> { - try { - if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) { - count.incrementAndGet(); - } - } catch (Throwable e) { - incomplete.set(true); - failure.compareAndSet(null, e); - } finally { - entityLatch.countDown(); - } - })) { - incomplete.set(true); - entityLatch.countDown(); - } - } - - if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) { - if (!Thread.currentThread().isInterrupted()) { - reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get()); - } - return null; - } - - return count.get(); - } - - static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) { - try { - return latch.await(timeout, unit); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; - } - } - - private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) { - if (world == null) { - return false; - } - - CompletableFuture future = new CompletableFuture<>(); - AtomicBoolean failureReported = new AtomicBoolean(); - future.whenComplete((ignored, failure) -> { - if (failure != null) { - reportSpawnFailure(chunkX, chunkZ, failure, failureReported); - } - }); - boolean scheduled; - try { - scheduled = J.runRegion(world, chunkX, chunkZ, () -> { - try { - if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { - future.complete(null); - return; - } - - spawnIn(world.getChunkAt(chunkX, chunkZ), initial); - future.complete(null); - } catch (Throwable e) { - future.completeExceptionally(e); - } - }); - } catch (Throwable e) { - IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e); - return false; - } - - if (!scheduled) { - IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + "."); - return false; - } - - try { - future.get(5, TimeUnit.SECONDS); - return true; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; - } catch (TimeoutException e) { - IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ); - return false; - } catch (ExecutionException e) { - Throwable cause = e.getCause() == null ? e : e.getCause(); - reportSpawnFailure(chunkX, chunkZ, cause, failureReported); - return false; - } - } - - private void reportEntityCountFailure(String message, Throwable error) { - entityCountValid = false; - if (error != null) { - if (entityCountErrorReported.compareAndSet(false, true)) { - IrisLogging.reportError(message, error); - } - return; - } - - if (entityCountWarningReported.compareAndSet(false, true)) { - IrisLogging.warn(message); - } - } - - private void resetEntityCountFailures() { - entityCountWarningReported.set(false); - entityCountErrorReported.set(false); - } - - private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) { - if (!failureReported.compareAndSet(false, true)) { - return; - } - Throwable cause = failure.getCause() == null ? failure : failure.getCause(); - IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause); - } - - private void spawnIn(Chunk c, boolean initial) { - if (getEngine().isClosed()) { - return; - } - - if (!isEntitySpawningEnabledForCurrentWorld()) { - return; - } - - IrisComplex complex = getEngine().getComplex(); - if (complex == null) { - return; - } - - if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { - forEachMarkerSpawner(c, (block, spawners) -> { - IrisSpawner s = new KList<>(spawners).getRandom(); - if (s == null) { - return; - } - - spawn(block, s, false); - J.runRegion(c.getWorld(), c.getX(), c.getZ(), managedTask( - "bukkit_world_manager_marker_spawn_followup", - () -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(), - () -> spawn(block, s, true)))); - }); - } - - if (!IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) { - return; - } - - //@builder - Predicate filter = i -> i.canSpawn(getEngine(), c.getX(), c.getZ()); - ChunkCounter counter = new ChunkCounter(c.getEntities()); - - IrisBiome biome = EngineBukkitOps.getSurfaceBiome(getEngine(), c); - IrisEntitySpawn v = spawnRandomly(Stream.concat(getData().getSpawnerLoader() - .loadAll(getDimension().getEntitySpawners()) - .shuffleCopy(RNG.r) - .stream() - .filter(filter) - .filter((i) -> i.isValid(biome)), - Stream.concat(getData() - .getSpawnerLoader() - .loadAll(getEngine().getRegion(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners()) - .shuffleCopy(RNG.r) - .stream() - .filter(filter), - getData().getSpawnerLoader() - .loadAll(getEngine().getSurfaceBiome(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners()) - .shuffleCopy(RNG.r) - .stream() - .filter(filter))) - .filter(counter) - .flatMap((i) -> stream(i, initial)) - .collect(Collectors.toList())) - .getRandom(); - //@done - if (v == null || v.getReferenceSpawner() == null) - return; - - spawn(c, v); - } - - private void spawn(Chunk c, IrisEntitySpawn i) { - IrisSpawner ref = i.getReferenceSpawner(); - int s = i.spawn(getEngine(), c, RNG.r); - actuallySpawned.addAndGet(s); - if (s > 0) { - ref.spawn(getEngine(), c.getX(), c.getZ()); - } - } - - private void spawn(IrisPosition pos, IrisEntitySpawn i) { - IrisSpawner ref = i.getReferenceSpawner(); - if (!ref.canSpawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()))) - return; - - int s = i.spawn(getEngine(), pos, RNG.r); - actuallySpawned.addAndGet(s); - if (s > 0) { - ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ())); - } - } - - private Stream stream(IrisSpawner s, boolean initial) { - for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) { - i.setReferenceSpawner(s); - i.setReferenceMarker(s.getReferenceMarker()); - } - - return (initial ? s.getInitialSpawns() : s.getSpawns()).stream(); - } - - private boolean isEntitySpawningEnabledForCurrentWorld() { - if (!getEngine().isStudio()) { - return true; - } - - return IrisSettings.get().getStudio().isEntitySpawning(); - } - - private KList spawnRandomly(List types) { - KList rarityTypes = new KList<>(); - int totalRarity = 0; - - for (IrisEntitySpawn i : types) { - totalRarity += IRare.get(i); - } - - for (IrisEntitySpawn i : types) { - rarityTypes.addMultiple(i, totalRarity / IRare.get(i)); - } - - return rarityTypes; + AtomicBoolean ignoreTeleport() { + return ignoreTP; } @Override @@ -1017,12 +210,32 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return; } + if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) { + return; + } + int cX = e.getX(), cZ = e.getZ(); Long key = Cache.key(cX, cZ); - cleanup.put(key, cleanupService.schedule(managedTask("bukkit_world_manager_chunk_cleanup", () -> { - cleanup.remove(key); + long delay = Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0); + final Future[] self = new Future[1]; + Runnable forget = () -> cleanup.remove(key, self[0]); + Runnable task = managedTask("bukkit_world_manager_chunk_cleanup", () -> { + forget.run(); getEngine().cleanupMantleChunk(cX, cZ); - }, () -> cleanup.remove(key)), Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS)); + }, forget); + + try { + // compute() keys the bin so the task cannot drop a newer mapping than its own. + cleanup.compute(key, (k, displaced) -> { + if (displaced != null) { + displaced.cancel(false); + } + + return self[0] = cleanupService.schedule(task, delay, TimeUnit.MILLISECONDS); + }); + } catch (RejectedExecutionException ex) { + IrisLogging.debug("Skipped mantle cleanup schedule for " + cX + ", " + cZ + "; cleanup executor is stopped."); + } } @Override @@ -1033,339 +246,23 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } } - private void spawn(IrisPosition block, IrisSpawner spawner, boolean initial) { - if (getEngine().isClosed()) { - return; - } - - if (spawner == null) { - return; - } - - KList s = initial ? spawner.getInitialSpawns() : spawner.getSpawns(); - if (s.isEmpty()) { - return; - } - - IrisEntitySpawn ss = spawnRandomly(s).getRandom(); - ss.setReferenceSpawner(spawner); - ss.setReferenceMarker(spawner.getReferenceMarker()); - spawn(block, ss); - } - public Mantle getMantle() { return getEngine().getMantle().getMantle(); } @Override public void teleportAsync(PlayerTeleportEvent e) { - e.setCancelled(true); - warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), managedTask( - "bukkit_world_manager_teleport", - () -> { - ignoreTP.set(true); - e.getPlayer().teleport(e.getTo(), e.getCause()); - ignoreTP.set(false); - }))); - } - - private void warmupAreaAsync(Player player, Location to, Runnable r) { - J.a(managedTask("bukkit_world_manager_teleport_warmup", () -> { - int viewDistance = 2; - KList> futures = new KList<>(); - for (int i = -viewDistance; i <= viewDistance; i++) { - for (int j = -viewDistance; j <= viewDistance; j++) { - int finalJ = j; - int finalI = i; - - if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) { - futures.add(CompletableFuture.completedFuture(null)); - continue; - } - - futures.add(MultiBurst.burst.completeValue(() - -> PaperLib.getChunkAtAsync(to.getWorld(), - (to.getBlockX() >> 4) + finalI, - (to.getBlockZ() >> 4) + finalJ, - true, false).get())); - } - } - - new QueueJob>() { - @Override - public void execute(Future chunkFuture) { - try { - chunkFuture.get(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk."); - } catch (ExecutionException ex) { - IrisLogging.reportError(ex); - } - } - - @Override - public String getName() { - return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS); - } - }.queue(futures).execute(new VolmitSender(player), true, r); - })); - } - - public Map> getSpawnersFromMarkers(Chunk c) { - Map> p = new KMap<>(); - Set b = new KSet<>(); - - if (J.isFolia()) { - if (!getMantle().isChunkLoaded(c.getX(), c.getZ())) { - warmupMantleChunkAsync(c.getX(), c.getZ()); - } - return p; - } - - getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> { - if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { - return; - } - - IrisMarker mark = getData().getMarkerLoader().load(t.getTag()); - if (mark == null) { - return; - } - - IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z); - - if (isMarkerObstructed(c, pos, mark.isEmptyAbove())) { - b.add(pos); - return; - } - - for (String i : mark.getSpawners()) { - IrisSpawner m = getData().getSpawnerLoader().load(i); - if (m == null) { - IrisLogging.error("Cannot load spawner: " + i + " for marker on " + getName()); - continue; - } - m.setReferenceMarker(mark); - - // This is so fucking incorrect its a joke - //noinspection ConstantConditions - if (m != null) { - p.computeIfAbsent(pos, (k) -> new KSet<>()).add(m); - } - } - }); - - for (IrisPosition i : b) { - getEngine().getMantle().getMantle().remove(i.getX(), i.getY(), i.getZ(), MatterMarker.class); - } - - return p; - } - - private void forEachMarkerSpawner(Chunk c, BiConsumer> consumer) { - if (c == null || consumer == null) { - return; - } - - if (!J.isFolia()) { - int minY = getEngine().getWorld().minHeight(); - getSpawnersFromMarkers(c).forEach((relative, spawners) -> { - if (spawners.isEmpty()) { - return; - } - - consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), spawners); - }); - return; - } - - int chunkX = c.getX(); - int chunkZ = c.getZ(); - World world = c.getWorld(); - long key = Cache.key(chunkX, chunkZ); - if (!markerScanQueue.add(key)) { - return; - } - - J.a(managedTask("bukkit_world_manager_marker_scan", () -> { - try { - Map markerData = collectMarkerSpawnData(chunkX, chunkZ); - if (markerData.isEmpty()) { - return; - } - - J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_marker_scan_region", () -> { - if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { - return; - } - - Chunk chunk = world.getChunkAt(chunkX, chunkZ); - int minY = getEngine().getWorld().minHeight(); - markerData.forEach((relative, data) -> { - if (data.spawners.isEmpty()) { - return; - } - - if (isMarkerObstructed(chunk, relative, data.requiresEmptyAbove)) { - removeMarkerAsync(relative); - return; - } - - consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners); - }); - })); - } catch (Throwable e) { - IrisLogging.reportError(e); - } finally { - markerScanQueue.remove(key); - } - }, () -> markerScanQueue.remove(key))); - } - - private Map collectMarkerSpawnData(int chunkX, int chunkZ) { - Map markerData = new KMap<>(); - getMantle().iterateChunk(chunkX, chunkZ, MatterMarker.class, (x, y, z, t) -> { - if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { - return; - } - - IrisMarker mark = getData().getMarkerLoader().load(t.getTag()); - if (mark == null) { - return; - } - - IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z); - MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData()); - data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove(); - - for (String i : mark.getSpawners()) { - IrisSpawner spawner = getData().getSpawnerLoader().load(i); - if (spawner == null) { - IrisLogging.error("Cannot load spawner: " + i + " for marker on " + getName()); - continue; - } - spawner.setReferenceMarker(mark); - data.spawners.add(spawner); - } - }); - - return markerData; - } - - private boolean isMarkerObstructed(Chunk chunk, IrisPosition relative, boolean requiresEmptyAbove) { - if (!requiresEmptyAbove) { - return false; - } - - int minY = getEngine().getWorld().minHeight(); - int markerY = toWorldY(relative.getY(), minY); - if (markerY + 2 >= chunk.getWorld().getMaxHeight()) { - return true; - } - - int localX = relative.getX() & 15; - int localZ = relative.getZ() & 15; - return chunk.getBlock(localX, markerY + 1, localZ).getBlockData().getMaterial().isSolid() - || chunk.getBlock(localX, markerY + 2, localZ).getBlockData().getMaterial().isSolid(); - } - - private void removeMarkerAsync(IrisPosition marker) { - J.a(managedTask("bukkit_world_manager_remove_marker", () -> { - try { - getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - })); - } - - private static final class MarkerSpawnData { - private final KSet spawners = new KSet<>(); - private boolean requiresEmptyAbove; + teleportWarmup.teleportAsync(e); } @Override public void onBlockBreak(BlockBreakEvent e) { - if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) { - int blockX = e.getBlock().getX(); - int mantleY = toMantleY(e.getBlock().getY(), getEngine().getWorld().minHeight()); - int blockZ = e.getBlock().getZ(); - - KList d = new KList<>(); - IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation()); - List dropProviders = filterDrops(b.getBlockDrops(), e, getData()); - - if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) { - IrisRegion r = EngineBukkitOps.getRegion(getEngine(), e.getBlock().getLocation()); - dropProviders.addAll(filterDrops(r.getBlockDrops(), e, getData())); - dropProviders.addAll(filterDrops(getEngine().getDimension().getBlockDrops(), e, getData())); - } - - dropProviders.forEach(provider -> provider.fillDrops(false, d)); - - if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) { - e.setDropItems(false); - } - - World w = e.getBlock().getWorld(); - Location blockLocation = e.getBlock().getLocation(); - Location dropLocation = blockLocation.clone().add(.5, .5, .5); - BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null; - Runnable finalizedBreak = managedTask("bukkit_world_manager_block_break_finalize", () -> { - if (e.isCancelled()) { - return; - } - J.a(managedTask("bukkit_world_manager_block_break_marker", () -> { - MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class); - if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) { - return; - } - - IrisMarker mark = getData().getMarkerLoader().load(marker.getTag()); - if (mark == null || mark.isRemoveOnChange()) { - getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class); - } - })); - routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item)); - }); - if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) { - J.s(finalizedBreak, 1); - } - } - } - - static int toMantleY(int worldY, int minHeight) { - return worldY - minHeight; - } - - static void routeDrops(Iterable drops, BlockDropRouter router, Consumer fallback) { - for (T drop : drops) { - boolean routed = false; - if (router != null) { - try { - routed = router.routeDrop(drop); - } catch (Throwable error) { - IrisLogging.reportError("Failed to route a deferred Iris block drop.", error); - } - } - if (!routed) { - fallback.accept(drop); - } - } - } - - static int toWorldY(int mantleY, int minHeight) { - return mantleY + minHeight; - } - - private List filterDrops(KList drops, BlockBreakEvent e, IrisData data) { - return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList()); + blockDropRouter.onBlockBreak(e); } @Override public void onBlockPlace(BlockPlaceEvent e) { - + blockDropRouter.onBlockPlace(e); } @Override @@ -1378,20 +275,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } if (!looperStopped) { try { + looperStopped = true; if (looper != null) { looper.interrupt(); + joinQuietly(looper); } - looperStopped = true; } catch (Throwable e) { failure = appendCloseFailure(failure, e); } } if (!cleanupServiceStopped) { try { + cleanupServiceStopped = true; if (cleanupService != null) { cleanupService.shutdownNow(); + awaitQuietly(cleanupService); } - cleanupServiceStopped = true; } catch (Throwable e) { failure = appendCloseFailure(failure, e); } @@ -1401,6 +300,31 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } } + private void joinQuietly(Thread thread) { + if (thread == Thread.currentThread()) { + return; + } + + try { + thread.join(CLOSE_AWAIT_MS); + if (thread.isAlive()) { + IrisLogging.warn("Thread " + thread.getName() + " did not stop within " + CLOSE_AWAIT_MS + "ms."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void awaitQuietly(ScheduledExecutorService service) { + try { + if (!service.awaitTermination(CLOSE_AWAIT_MS, TimeUnit.MILLISECONDS)) { + IrisLogging.warn("Mantle cleanup executor did not stop within " + CLOSE_AWAIT_MS + "ms."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + @Override public int getChunkCount() { return loadedChunkCount; @@ -1424,27 +348,4 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } return failure; } - - @Data - private static class ChunkCounter implements Predicate { - private final Entity[] entities; - private transient int index = 0; - private transient int count = 0; - - @Override - public boolean test(IrisSpawner spawner) { - int max = spawner.getMaxEntitiesPerChunk(); - if (max <= count) - return false; - - while (index < entities.length) { - if (entities[index++] instanceof LivingEntity) { - if (++count >= max) - return false; - } - } - - return true; - } - } } diff --git a/core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java b/core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java new file mode 100644 index 000000000..5abb03270 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/MarkerSpawnScanner.java @@ -0,0 +1,224 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.matter.MatterMarker; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; + +/** + * Resolves the mantle spawn markers of a chunk into spawners. On Folia the mantle read runs + * asynchronously and only the obstruction check and the consumer callback hop back onto the region + * thread that owns the chunk; obstructed markers are removed off-thread. + */ +final class MarkerSpawnScanner { + private final IrisWorldManager manager; + private final Set markerScanQueue = ConcurrentHashMap.newKeySet(); + + MarkerSpawnScanner(IrisWorldManager manager) { + this.manager = manager; + } + + Map> getSpawnersFromMarkers(Chunk c) { + Map> p = new KMap<>(); + Set b = new KSet<>(); + + if (J.isFolia()) { + if (!manager.getMantle().isChunkLoaded(c.getX(), c.getZ())) { + manager.chunkMaintenance.warmupMantleChunkAsync(c.getX(), c.getZ()); + } + return p; + } + + manager.getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> { + if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag()); + if (mark == null) { + return; + } + + IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z); + + if (isMarkerObstructed(c, pos, mark.isEmptyAbove())) { + b.add(pos); + return; + } + + for (String i : mark.getSpawners()) { + IrisSpawner m = manager.getData().getSpawnerLoader().load(i); + if (m == null) { + IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName()); + continue; + } + m.setReferenceMarker(mark); + + // This is so fucking incorrect its a joke + //noinspection ConstantConditions + if (m != null) { + p.computeIfAbsent(pos, (k) -> new KSet<>()).add(m); + } + } + }); + + for (IrisPosition i : b) { + manager.getEngine().getMantle().getMantle().remove(i.getX(), i.getY(), i.getZ(), MatterMarker.class); + } + + return p; + } + + void forEachMarkerSpawner(Chunk c, BiConsumer> consumer) { + if (c == null || consumer == null) { + return; + } + + if (!J.isFolia()) { + int minY = manager.getEngine().getWorld().minHeight(); + getSpawnersFromMarkers(c).forEach((relative, spawners) -> { + if (spawners.isEmpty()) { + return; + } + + consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), spawners); + }); + return; + } + + int chunkX = c.getX(); + int chunkZ = c.getZ(); + World world = c.getWorld(); + long key = Cache.key(chunkX, chunkZ); + if (!markerScanQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> { + try { + Map markerData = collectMarkerSpawnData(chunkX, chunkZ); + if (markerData.isEmpty()) { + return; + } + + J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_marker_scan_region", () -> { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + int minY = manager.getEngine().getWorld().minHeight(); + markerData.forEach((relative, data) -> { + if (data.spawners.isEmpty()) { + return; + } + + if (isMarkerObstructed(chunk, relative, data.requiresEmptyAbove)) { + removeMarkerAsync(relative); + return; + } + + consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners); + }); + })); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + markerScanQueue.remove(key); + } + }, () -> markerScanQueue.remove(key))); + } + + private Map collectMarkerSpawnData(int chunkX, int chunkZ) { + Map markerData = new KMap<>(); + manager.getMantle().iterateChunk(chunkX, chunkZ, MatterMarker.class, (x, y, z, t) -> { + if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag()); + if (mark == null) { + return; + } + + IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z); + MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData()); + data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove(); + + for (String i : mark.getSpawners()) { + IrisSpawner spawner = manager.getData().getSpawnerLoader().load(i); + if (spawner == null) { + IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName()); + continue; + } + spawner.setReferenceMarker(mark); + data.spawners.add(spawner); + } + }); + + return markerData; + } + + private boolean isMarkerObstructed(Chunk chunk, IrisPosition relative, boolean requiresEmptyAbove) { + if (!requiresEmptyAbove) { + return false; + } + + int minY = manager.getEngine().getWorld().minHeight(); + int markerY = WorldBlockDropRouter.toWorldY(relative.getY(), minY); + if (markerY + 2 >= chunk.getWorld().getMaxHeight()) { + return true; + } + + int localX = relative.getX() & 15; + int localZ = relative.getZ() & 15; + return chunk.getBlock(localX, markerY + 1, localZ).getBlockData().getMaterial().isSolid() + || chunk.getBlock(localX, markerY + 2, localZ).getBlockData().getMaterial().isSolid(); + } + + private void removeMarkerAsync(IrisPosition marker) { + J.a(manager.managedTask("bukkit_world_manager_remove_marker", () -> { + try { + manager.getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + })); + } + + private static final class MarkerSpawnData { + private final KSet spawners = new KSet<>(); + private boolean requiresEmptyAbove; + } +} 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 34fc8216e..8df80bd79 100644 --- a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java +++ b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java @@ -14,7 +14,7 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.DataProvider; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBounds; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.interpolation.Interpolated; diff --git a/core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java b/core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java new file mode 100644 index 000000000..9d1b69a46 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldBlockDropRouter.java @@ -0,0 +1,134 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.service.tree.BlockDropRouter; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBlockDrops; +import art.arcane.iris.engine.object.IrisMarker; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.matter.MatterMarker; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Biome, region and dimension block drops for a Bukkit Iris world. The drop list is resolved on the + * event thread, the marker cleanup runs asynchronously and the drops themselves are handed to the + * event's own router when it offers one so a deferred break keeps its inventory destination. + */ +final class WorldBlockDropRouter { + private final IrisWorldManager manager; + + WorldBlockDropRouter(IrisWorldManager manager) { + this.manager = manager; + } + + void onBlockBreak(BlockBreakEvent e) { + if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) { + int blockX = e.getBlock().getX(); + int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight()); + int blockZ = e.getBlock().getZ(); + + KList d = new KList<>(); + IrisBiome b = EngineBukkitOps.getBiome(manager.getEngine(), e.getBlock().getLocation()); + List dropProviders = filterDrops(b.getBlockDrops(), e, manager.getData()); + + if (dropProviders.stream().noneMatch(IrisBlockDrops::isSkipParents)) { + IrisRegion r = EngineBukkitOps.getRegion(manager.getEngine(), e.getBlock().getLocation()); + dropProviders.addAll(filterDrops(r.getBlockDrops(), e, manager.getData())); + dropProviders.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), e, manager.getData())); + } + + dropProviders.forEach(provider -> provider.fillDrops(false, d)); + + if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) { + e.setDropItems(false); + } + + World w = e.getBlock().getWorld(); + Location blockLocation = e.getBlock().getLocation(); + Location dropLocation = blockLocation.clone().add(.5, .5, .5); + BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null; + Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> { + if (e.isCancelled()) { + return; + } + J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> { + MatterMarker marker = manager.getMantle().get(blockX, mantleY, blockZ, MatterMarker.class); + if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) { + return; + } + + IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag()); + if (mark == null || mark.isRemoveOnChange()) { + manager.getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class); + } + })); + routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item)); + }); + if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) { + J.s(finalizedBreak, 1); + } + } + } + + void onBlockPlace(BlockPlaceEvent e) { + + } + + static int toMantleY(int worldY, int minHeight) { + return worldY - minHeight; + } + + static void routeDrops(Iterable drops, BlockDropRouter router, Consumer fallback) { + for (T drop : drops) { + boolean routed = false; + if (router != null) { + try { + routed = router.routeDrop(drop); + } catch (Throwable error) { + IrisLogging.reportError("Failed to route a deferred Iris block drop.", error); + } + } + if (!routed) { + fallback.accept(drop); + } + } + } + + static int toWorldY(int mantleY, int minHeight) { + return mantleY + minHeight; + } + + private List filterDrops(KList drops, BlockBreakEvent e, IrisData data) { + return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java b/core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java new file mode 100644 index 000000000..fb57aa84d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldChunkMaintenance.java @@ -0,0 +1,405 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Chunk discovery, post-load block updates and mantle warmup for a Bukkit Iris world. Every scan is + * scheduled from the global thread, fans out per player onto the owning entity thread and touches a + * chunk only from its own region thread; the scan-scheduled flags and per-chunk key sets keep a + * single pass in flight per chunk and are always cleared on the rejection path. + */ +final class WorldChunkMaintenance { + private static final int MAX_FORCED_CHUNK_UPDATES = 128; + + private final IrisWorldManager manager; + private final Set mantleWarmupQueue = ConcurrentHashMap.newKeySet(); + private final Set markerFlagQueue = ConcurrentHashMap.newKeySet(); + private final Set discoveredFlagQueue = ConcurrentHashMap.newKeySet(); + private final Set chunkUpdateQueue = ConcurrentHashMap.newKeySet(); + private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean(); + private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean(); + private int forcedChunkUpdateCursor = 0; + + WorldChunkMaintenance(IrisWorldManager manager) { + this.manager = manager; + } + + void discoverChunks() { + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return; + } + + if (manager.entitySpawner.isPregenActiveForThisWorld()) { + return; + } + + if (!chunkDiscoveryScanScheduled.compareAndSet(false, true)) { + return; + } + + boolean scheduled = J.runGlobal(manager.managedTask("bukkit_world_manager_discover_chunks", () -> { + try { + if (manager.getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(manager.getEngine().getWorld()))) { + return; + } + + for (Player player : world.getPlayers()) { + if (player == null) { + continue; + } + + J.runEntity(player, manager.managedTask("bukkit_world_manager_discover_player", () -> { + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + + int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX()); + int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ()); + int radius = 1; + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + int chunkX = centerX + x; + int chunkZ = centerZ + z; + raiseDiscoveredChunkFlag(world, chunkX, chunkZ); + } + } + })); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + chunkDiscoveryScanScheduled.set(false); + } + }, () -> chunkDiscoveryScanScheduled.set(false))); + if (!scheduled) { + chunkDiscoveryScanScheduled.set(false); + } + } + + private void raiseDiscoveredChunkFlag(World world, int chunkX, int chunkZ) { + if (world == null) { + return; + } + + if (!J.isFolia()) { + manager.getMantle().getChunk(chunkX, chunkZ).flag(MantleFlag.DISCOVERED, true); + return; + } + + long key = Cache.key(chunkX, chunkZ); + if (!discoveredFlagQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_discovered_flag", () -> { + try { + Mantle mantle = manager.getMantle(); + if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) { + mantle.flag(chunkX, chunkZ, MantleFlag.DISCOVERED, true); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + discoveredFlagQueue.remove(key); + } + }, () -> discoveredFlagQueue.remove(key))); + } + + void updateChunks() { + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return; + } + + if (manager.entitySpawner.isPregenActiveForThisWorld()) { + return; + } + + if (!chunkUpdateScanScheduled.compareAndSet(false, true)) { + return; + } + + boolean scheduled = J.runGlobal(manager.managedTask( + "bukkit_world_manager_update_chunks", + () -> updateChunksOnGlobal(world), + () -> chunkUpdateScanScheduled.set(false))); + if (!scheduled) { + chunkUpdateScanScheduled.set(false); + } + } + + private void updateChunksOnGlobal(World world) { + try { + if (manager.getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(manager.getEngine().getWorld()))) { + return; + } + + List players = new ArrayList<>(world.getPlayers()); + manager.playersPresent = !players.isEmpty(); + manager.loadedChunkCount = world.getLoadedChunks().length; + for (Player player : players) { + if (player == null) { + continue; + } + + J.runEntity(player, manager.managedTask( + "bukkit_world_manager_player_chunk_updates", + () -> schedulePlayerChunkUpdates(world, player))); + } + + scheduleForcedChunkUpdates(world); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + chunkUpdateScanScheduled.set(false); + } + } + + private void schedulePlayerChunkUpdates(World world, Player player) { + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + + int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX()); + int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ()); + int radius = 1; + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + scheduleChunkUpdate(world, centerX + x, centerZ + z); + } + } + } + + private void scheduleForcedChunkUpdates(World world) { + List forcedChunks = new ArrayList<>(); + for (Chunk chunk : world.getForceLoadedChunks()) { + forcedChunks.add(new Position2(chunk.getX(), chunk.getZ())); + } + forcedChunks.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ)); + + int forcedChunkCount = forcedChunks.size(); + if (forcedChunkCount == 0) { + forcedChunkUpdateCursor = 0; + return; + } + + int updateCount = Math.min(forcedChunkCount, MAX_FORCED_CHUNK_UPDATES); + int start = Math.floorMod(forcedChunkUpdateCursor, forcedChunkCount); + for (int i = 0; i < updateCount; i++) { + Position2 chunk = forcedChunks.get((start + i) % forcedChunkCount); + scheduleChunkUpdate(world, chunk.getX(), chunk.getZ()); + } + forcedChunkUpdateCursor = (start + updateCount) % forcedChunkCount; + } + + private void scheduleChunkUpdate(World world, int chunkX, int chunkZ) { + long key = Cache.key(chunkX, chunkZ); + if (!chunkUpdateQueue.add(key)) { + return; + } + + try { + boolean scheduled = J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_chunk_update", () -> { + try { + updateChunkRegion(world, chunkX, chunkZ); + } finally { + chunkUpdateQueue.remove(key); + } + }, () -> chunkUpdateQueue.remove(key))); + if (!scheduled) { + chunkUpdateQueue.remove(key); + } + } catch (Throwable e) { + chunkUpdateQueue.remove(key); + IrisLogging.reportError(e); + } + } + + private void updateChunkRegion(World world, int chunkX, int chunkZ) { + if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + + if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { + if (!manager.getMantle().isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunkAsync(chunkX, chunkZ); + return; + } + EngineBukkitOps.updateChunk(manager.getEngine(), chunk); + } + + if (!manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) { + return; + } + + if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { + return; + } + + if (!J.isFolia() && !manager.getMantle().isChunkLoaded(chunkX, chunkZ)) { + warmupMantleChunkAsync(chunkX, chunkZ); + return; + } + + raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> { + int delay = RNG.r.i(5, 200); + J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_initial_spawn_followup", () -> { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return; + } + manager.entitySpawner.spawnIn(world.getChunkAt(chunkX, chunkZ), true); + }), delay); + + Chunk markerChunk = world.getChunkAt(chunkX, chunkZ); + manager.markerScanner.forEachMarkerSpawner(markerChunk, (block, spawners) -> { + IrisSpawner s = new KList<>(spawners).getRandom(); + if (s == null) { + return; + } + manager.entitySpawner.spawn(block, s, true); + }); + }); + } + + void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) { + if (world == null || onFirstRaise == null) { + return; + } + + if (!J.isFolia()) { + manager.getMantle().raiseFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, onFirstRaise); + return; + } + + long key = Cache.key(chunkX, chunkZ); + if (!markerFlagQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_spawn_marker_flag", () -> { + boolean raised = false; + try { + Mantle mantle = manager.getMantle(); + if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) { + mantle.flag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER, true); + raised = true; + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + markerFlagQueue.remove(key); + } + + if (!raised) { + return; + } + + J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_spawn_marker_callback", () -> { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + return; + } + onFirstRaise.run(); + })); + }, () -> markerFlagQueue.remove(key))); + } + + void warmupMantleChunkAsync(int chunkX, int chunkZ) { + long key = Cache.key(chunkX, chunkZ); + if (!mantleWarmupQueue.add(key)) { + return; + } + + J.a(manager.managedTask("bukkit_world_manager_mantle_warmup", () -> { + try { + manager.getMantle().getChunk(chunkX, chunkZ); + } catch (Throwable e) { + IrisLogging.reportError(e); + } finally { + mantleWarmupQueue.remove(key); + } + }, () -> mantleWarmupQueue.remove(key))); + } + + Position2[] getLoadedChunkPositionsSnapshot(World world) { + if (world == null) { + return new Position2[0]; + } + + CompletableFuture future = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + Chunk[] chunks = world.getLoadedChunks(); + Position2[] positions = new Position2[chunks.length]; + for (int i = 0; i < chunks.length; i++) { + positions[i] = new Position2(chunks[i].getX(), chunks[i].getZ()); + } + manager.loadedChunkCount = positions.length; + future.complete(positions); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + if (!scheduled) { + return new Position2[0]; + } + + try { + return future.get(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new Position2[0]; + } catch (ExecutionException | TimeoutException e) { + IrisLogging.reportError(e); + return new Position2[0]; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java b/core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java new file mode 100644 index 000000000..88e9d732d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldEntitySpawner.java @@ -0,0 +1,558 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.gui.PregeneratorJob; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.object.IRare; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisEntitySpawn; +import art.arcane.iris.engine.object.IrisPosition; +import art.arcane.iris.engine.object.IrisSpawner; +import art.arcane.iris.engine.platform.EngineBukkitOps; +import art.arcane.iris.platform.bukkit.BukkitWorldBinding; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.plugin.Chunks; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.bukkit.WorldIdentity; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import lombok.Data; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Ambient and marker entity spawning for a Bukkit Iris world. Every count and spawn hops onto the + * thread that owns the data it reads (global for whole-world scans, entity for Folia candidate + * scans, region for the chunk being populated) and spawning is paused whenever a count could not + * be completed, so an incomplete saturation reading can never authorize a spawn. + */ +final class WorldEntitySpawner { + private final IrisWorldManager manager; + private final AtomicInteger actuallySpawned = new AtomicInteger(); + private final AtomicBoolean entityCountWarningReported = new AtomicBoolean(); + private final AtomicBoolean entityCountErrorReported = new AtomicBoolean(); + private int cooldown = 0; + + WorldEntitySpawner(IrisWorldManager manager) { + this.manager = manager; + } + + boolean onAsyncTick() { + if (manager.getEngine().isClosing() || manager.getEngine().isClosed()) { + return false; + } + + if (isPregenActiveForThisWorld()) { + J.sleep(500); + return false; + } + + actuallySpawned.set(0); + + if (!manager.getEngine().getWorld().hasPlatformWorld()) { + IrisLogging.debug("Can't spawn. No real world"); + J.sleep(5000); + return false; + } + + if (manager.cl.flip()) { + try { + World realWorld = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (realWorld == null) { + manager.entityCount = 0; + manager.entityCountValid = false; + } else if (J.isFolia()) { + Integer count = getFoliaLivingEntityCount(realWorld); + if (count != null) { + manager.entityCount = count; + manager.entityCountValid = true; + resetEntityCountFailures(); + } else { + manager.entityCountValid = false; + } + } else { + CompletableFuture future = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + int count = 0; + for (Entity entity : realWorld.getEntities()) { + if (entity instanceof LivingEntity && !entity.isDead()) { + count++; + } + } + future.complete(count); + } catch (Throwable ex) { + future.completeExceptionally(ex); + } + }); + if (scheduled) { + manager.entityCount = future.get(2, TimeUnit.SECONDS); + manager.entityCountValid = true; + resetEntityCountFailures(); + } else { + reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null); + } + } + } catch (InterruptedException e) { + manager.entityCountValid = false; + Thread.currentThread().interrupt(); + return false; + } catch (TimeoutException e) { + reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null); + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause); + } catch (Throwable e) { + reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e); + } + } + + if (!manager.entityCountValid) { + return false; + } + + double epx = manager.getEntitySaturation(); + if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) { + IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + manager.entityCount + ")"); + J.sleep(5000); + return false; + } + + int spawnBuffer = RNG.r.i(2, 12); + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return false; + } + + Position2[] cc = manager.chunkMaintenance.getLoadedChunkPositionsSnapshot(world); + while (spawnBuffer-- > 0) { + if (manager.getEngine().isClosing() || manager.getEngine().isClosed()) { + return actuallySpawned.get() > 0; + } + + if (cc.length == 0) { + IrisLogging.debug("Can't spawn. No chunks!"); + return false; + } + + Position2 c = cc[RNG.r.nextInt(cc.length)]; + if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) { + return actuallySpawned.get() > 0; + } + } + + return actuallySpawned.get() > 0; + } + + boolean isPregenActiveForThisWorld() { + World world = BukkitWorldBinding.world(manager.getEngine().getWorld()); + if (world == null) { + return false; + } + + if (IrisToolbelt.isWorldMaintenanceActive(world)) { + return true; + } + + PregeneratorJob job = PregeneratorJob.getInstance(); + if (job == null) { + return false; + } + + return job.targetsWorldIdentity(WorldIdentity.serialize(world)); + } + + private Integer getFoliaLivingEntityCount(World world) { + CompletableFuture> playerFuture = new CompletableFuture<>(); + boolean scheduled = J.runGlobal(() -> { + try { + playerFuture.complete(new ArrayList<>(world.getPlayers())); + } catch (Throwable e) { + playerFuture.completeExceptionally(e); + } + }); + if (!scheduled) { + reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null); + return null; + } + + List players; + try { + players = playerFuture.get(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + manager.entityCountValid = false; + Thread.currentThread().interrupt(); + return null; + } catch (TimeoutException e) { + reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null); + return null; + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause); + return null; + } + + Map candidates = new ConcurrentHashMap<>(); + AtomicBoolean incomplete = new AtomicBoolean(); + AtomicReference failure = new AtomicReference<>(); + + CountDownLatch latch = new CountDownLatch(players.size()); + for (Player player : players) { + if (player == null) { + latch.countDown(); + continue; + } + + if (!J.runEntity(player, () -> { + try { + if (!player.isOnline() || !world.equals(player.getWorld())) { + return; + } + candidates.put(player.getUniqueId().toString(), player); + for (Entity nearby : player.getNearbyEntities(64, 64, 64)) { + if (nearby != null) { + candidates.put(nearby.getUniqueId().toString(), nearby); + } + } + } catch (Throwable e) { + incomplete.set(true); + failure.compareAndSet(null, e); + } finally { + latch.countDown(); + } + })) { + incomplete.set(true); + latch.countDown(); + } + } + + if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) { + if (!Thread.currentThread().isInterrupted()) { + reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get()); + } + return null; + } + + AtomicInteger count = new AtomicInteger(); + incomplete.set(false); + failure.set(null); + CountDownLatch entityLatch = new CountDownLatch(candidates.size()); + for (Entity entity : candidates.values()) { + if (!J.runEntity(entity, () -> { + try { + if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) { + count.incrementAndGet(); + } + } catch (Throwable e) { + incomplete.set(true); + failure.compareAndSet(null, e); + } finally { + entityLatch.countDown(); + } + })) { + incomplete.set(true); + entityLatch.countDown(); + } + } + + if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) { + if (!Thread.currentThread().isInterrupted()) { + reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get()); + } + return null; + } + + return count.get(); + } + + static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) { + try { + return latch.await(timeout, unit); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) { + if (world == null) { + return false; + } + + CompletableFuture future = new CompletableFuture<>(); + AtomicBoolean failureReported = new AtomicBoolean(); + future.whenComplete((ignored, failure) -> { + if (failure != null) { + reportSpawnFailure(chunkX, chunkZ, failure, failureReported); + } + }); + boolean scheduled; + try { + scheduled = J.runRegion(world, chunkX, chunkZ, () -> { + try { + if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) { + future.complete(null); + return; + } + + spawnIn(world.getChunkAt(chunkX, chunkZ), initial); + future.complete(null); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + } catch (Throwable e) { + IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e); + return false; + } + + if (!scheduled) { + IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + "."); + return false; + } + + try { + future.get(5, TimeUnit.SECONDS); + return true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } catch (TimeoutException e) { + IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ); + return false; + } catch (ExecutionException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + reportSpawnFailure(chunkX, chunkZ, cause, failureReported); + return false; + } + } + + private void reportEntityCountFailure(String message, Throwable error) { + manager.entityCountValid = false; + if (error != null) { + if (entityCountErrorReported.compareAndSet(false, true)) { + IrisLogging.reportError(message, error); + } + return; + } + + if (entityCountWarningReported.compareAndSet(false, true)) { + IrisLogging.warn(message); + } + } + + private void resetEntityCountFailures() { + entityCountWarningReported.set(false); + entityCountErrorReported.set(false); + } + + private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) { + if (!failureReported.compareAndSet(false, true)) { + return; + } + Throwable cause = failure.getCause() == null ? failure : failure.getCause(); + IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause); + } + + void spawnIn(Chunk c, boolean initial) { + if (manager.getEngine().isClosed()) { + return; + } + + if (!isEntitySpawningEnabledForCurrentWorld()) { + return; + } + + IrisComplex complex = manager.getEngine().getComplex(); + if (complex == null) { + return; + } + + if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { + manager.markerScanner.forEachMarkerSpawner(c, (block, spawners) -> { + IrisSpawner s = new KList<>(spawners).getRandom(); + if (s == null) { + return; + } + + spawn(block, s, false); + J.runRegion(c.getWorld(), c.getX(), c.getZ(), manager.managedTask( + "bukkit_world_manager_marker_spawn_followup", + () -> manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(), + () -> spawn(block, s, true)))); + }); + } + + if (!IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) { + return; + } + + //@builder + Predicate filter = i -> i.canSpawn(manager.getEngine(), c.getX(), c.getZ()); + ChunkCounter counter = new ChunkCounter(c.getEntities()); + + IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c); + IrisEntitySpawn v = spawnRandomly(Stream.concat(manager.getData().getSpawnerLoader() + .loadAll(manager.getDimension().getEntitySpawners()) + .shuffleCopy(RNG.r) + .stream() + .filter(filter) + .filter((i) -> i.isValid(biome)), + Stream.concat(manager.getData() + .getSpawnerLoader() + .loadAll(manager.getEngine().getRegion(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners()) + .shuffleCopy(RNG.r) + .stream() + .filter(filter), + manager.getData().getSpawnerLoader() + .loadAll(manager.getEngine().getSurfaceBiome(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners()) + .shuffleCopy(RNG.r) + .stream() + .filter(filter))) + .filter(counter) + .flatMap((i) -> stream(i, initial)) + .collect(Collectors.toList())) + .getRandom(); + //@done + if (v == null || v.getReferenceSpawner() == null) + return; + + spawn(c, v); + } + + private void spawn(Chunk c, IrisEntitySpawn i) { + IrisSpawner ref = i.getReferenceSpawner(); + int s = i.spawn(manager.getEngine(), c, RNG.r); + actuallySpawned.addAndGet(s); + if (s > 0) { + ref.spawn(manager.getEngine(), c.getX(), c.getZ()); + } + } + + private void spawn(IrisPosition pos, IrisEntitySpawn i) { + IrisSpawner ref = i.getReferenceSpawner(); + if (!ref.canSpawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()))) + return; + + int s = i.spawn(manager.getEngine(), pos, RNG.r); + actuallySpawned.addAndGet(s); + if (s > 0) { + ref.spawn(manager.getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ())); + } + } + + void spawn(IrisPosition block, IrisSpawner spawner, boolean initial) { + if (manager.getEngine().isClosed()) { + return; + } + + if (spawner == null) { + return; + } + + KList s = initial ? spawner.getInitialSpawns() : spawner.getSpawns(); + if (s.isEmpty()) { + return; + } + + IrisEntitySpawn ss = spawnRandomly(s).getRandom(); + ss.setReferenceSpawner(spawner); + ss.setReferenceMarker(spawner.getReferenceMarker()); + spawn(block, ss); + } + + private Stream stream(IrisSpawner s, boolean initial) { + for (IrisEntitySpawn i : initial ? s.getInitialSpawns() : s.getSpawns()) { + i.setReferenceSpawner(s); + i.setReferenceMarker(s.getReferenceMarker()); + } + + return (initial ? s.getInitialSpawns() : s.getSpawns()).stream(); + } + + boolean isEntitySpawningEnabledForCurrentWorld() { + if (!manager.getEngine().isStudio()) { + return true; + } + + return IrisSettings.get().getStudio().isEntitySpawning(); + } + + private KList spawnRandomly(List types) { + KList rarityTypes = new KList<>(); + int totalRarity = 0; + + for (IrisEntitySpawn i : types) { + totalRarity += IRare.get(i); + } + + for (IrisEntitySpawn i : types) { + rarityTypes.addMultiple(i, totalRarity / IRare.get(i)); + } + + return rarityTypes; + } + + @Data + private static class ChunkCounter implements Predicate { + private final Entity[] entities; + private transient int index = 0; + private transient int count = 0; + + @Override + public boolean test(IrisSpawner spawner) { + int max = spawner.getMaxEntitiesPerChunk(); + if (max <= count) + return false; + + while (index < entities.length) { + if (entities[index++] instanceof LivingEntity) { + if (++count >= max) + return false; + } + } + + return true; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java b/core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java new file mode 100644 index 000000000..a98a74075 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/WorldTeleportWarmup.java @@ -0,0 +1,104 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.iris.util.common.scheduling.jobs.QueueJob; +import art.arcane.volmlib.util.collection.KList; +import io.papermc.lib.PaperLib; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * Cancels a teleport, loads the destination chunks off the main thread and then replays the teleport + * on the thread that owns the player. The replay sets the manager's ignore flag so the reissued + * teleport is not intercepted again. + */ +final class WorldTeleportWarmup { + private final IrisWorldManager manager; + + WorldTeleportWarmup(IrisWorldManager manager) { + this.manager = manager; + } + + void teleportAsync(PlayerTeleportEvent e) { + e.setCancelled(true); + warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), manager.managedTask( + "bukkit_world_manager_teleport", + () -> { + manager.ignoreTeleport().set(true); + e.getPlayer().teleport(e.getTo(), e.getCause()); + manager.ignoreTeleport().set(false); + }))); + } + + private void warmupAreaAsync(Player player, Location to, Runnable r) { + J.a(manager.managedTask("bukkit_world_manager_teleport_warmup", () -> { + int viewDistance = 2; + KList> futures = new KList<>(); + for (int i = -viewDistance; i <= viewDistance; i++) { + for (int j = -viewDistance; j <= viewDistance; j++) { + int finalJ = j; + int finalI = i; + + if (to.getWorld().isChunkLoaded((to.getBlockX() >> 4) + i, (to.getBlockZ() >> 4) + j)) { + futures.add(CompletableFuture.completedFuture(null)); + continue; + } + + futures.add(MultiBurst.burst.completeValue(() + -> PaperLib.getChunkAtAsync(to.getWorld(), + (to.getBlockX() >> 4) + finalI, + (to.getBlockZ() >> 4) + finalJ, + true, false).get())); + } + } + + new QueueJob>() { + @Override + public void execute(Future chunkFuture) { + try { + chunkFuture.get(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + IrisLogging.debug("Chunk warmup interrupted while loading async teleport chunk."); + } catch (ExecutionException ex) { + IrisLogging.reportError(ex); + } + } + + @Override + public String getName() { + return IrisLanguage.text(RuntimeUiMessages.JOB_LOADING_CHUNKS); + } + }.queue(futures).execute(new VolmitSender(player), true, r); + })); + } +} 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 19e2bb57b..663459f27 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 @@ -23,19 +23,25 @@ import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.context.ChunkedDataCache; +import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterBiomeInject; import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; import art.arcane.volmlib.util.scheduling.ChronoLatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBiome; public class IrisBiomeActuator extends EngineAssignedActuator { private final RNG rng; private final ChronoLatch cl = new ChronoLatch(5000); + private final KMap resolvedBiomes = new KMap<>(); public IrisBiomeActuator(Engine engine) { super(engine, "Biome"); @@ -47,36 +53,65 @@ public class IrisBiomeActuator extends EngineAssignedActuator { public void onActuate(int x, int z, Hunk h, boolean multicore, ChunkContext context) { try { PrecisionStopwatch p = PrecisionStopwatch.start(); - for (int xf = 0; xf < h.getWidth(); xf++) { + int width = h.getWidth(); + int depth = h.getDepth(); + int height = h.getHeight(); + Engine engine = getEngine(); + Mantle mantle = engine.getMantle().getMantle(); + ChunkedDataCache biomeCache = context.getBiome(); + + for (int xf = 0; xf < width; xf++) { IrisBiome ib; - for (int zf = 0; zf < h.getDepth(); zf++) { - ib = context.getBiome().get(xf, zf); - MatterBiomeInject matter; - PlatformBiome biome; + for (int zf = 0; zf < depth; zf++) { + ib = biomeCache.get(xf, zf); + String key; if (ib.isCustom()) { - IrisBiomeCustom custom = ib.getCustomBiome(rng, getEngine(), x + xf, 0, z + zf); - String key = getDimension().getLoadKey() + ":" + custom.getId(); - biome = IrisPlatforms.get().registries().biome(key); - matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(key)); + IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf); + key = getDimension().getLoadKey() + ":" + custom.getId(); } else { - String skyKey = ib.getSkyBiomeKey(rng, getEngine(), x + xf, 0, z + zf); - biome = IrisPlatforms.get().registries().biome(skyKey); - matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(skyKey)); + key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf); } + ResolvedBiome resolved = resolve(key); + PlatformBiome biome = resolved.biome(); + if (biome != null) { - for (int yf = 0; yf < h.getHeight(); yf++) { - h.set(xf, yf, zf, biome); - } + h.set(xf, 0, zf, xf, height - 1, zf, biome); } - getEngine().getMantle().getMantle().set(x + xf, 0, z + zf, matter); + mantle.set(x + xf, 0, z + zf, resolved.matter()); } } - getEngine().getMetrics().getBiome().put(p.getMilliseconds()); + engine.getMetrics().getBiome().put(p.getMilliseconds()); } catch (Throwable e) { e.printStackTrace(); } } + + /** + * The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair + * instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a + * biome registered later still resolves. + */ + private ResolvedBiome resolve(String key) { + ResolvedBiome cached = key == null ? null : resolvedBiomes.get(key); + + if (cached != null) { + return cached; + } + + IrisPlatform platform = IrisPlatforms.get(); + PlatformBiome biome = platform.registries().biome(key); + ResolvedBiome resolved = new ResolvedBiome(biome, BiomeInjectMatter.get(platform.biomeWriter().biomeIdFor(key))); + + if (key != null && biome != null) { + resolvedBiomes.put(key, resolved); + } + + return resolved; + } + + private record ResolvedBiome(PlatformBiome biome, MatterBiomeInject matter) { + } } diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java index d0990c6a6..2a1b4d5f4 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -84,128 +84,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator h, ChunkContext context) { - int zf, realX, realZ, hf, he; - IrisBiome biome; - IrisRegion region; - int clampedFluidHeight = Math.min(h.getHeight(), getDimension().getFluidHeight()); - - for (zf = 0; zf < h.getDepth(); zf++) { - realX = xf + x; - realZ = zf + z; - biome = context.getBiome().get(xf, zf); - region = context.getRegion().get(xf, zf); - he = Math.min(h.getHeight(), context.getRoundedHeight(xf, zf)); - hf = Math.max(clampedFluidHeight, he); - - if (hf < 0) { - continue; - } - - KList blocks = null; - KList fblocks = null; - int depth, fdepth; - for (int i = hf; i >= 0; i--) { - if (i >= h.getHeight()) { - continue; - } - - if (i == 0) { - if (getDimension().isBedrock()) { - h.setRaw(xf, i, zf, BEDROCK); - lastBedrock = i; - continue; - } - } - - PlatformBlockState ore = biome.generateOres(realX, i, realZ, rng, getData(), true); - ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore; - ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore; - if (ore != null) { - h.setRaw(xf, i, zf, ore); - continue; - } - - if (i > he && i <= hf) { - fdepth = hf - i; - - if (fblocks == null) { - fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, getData()); - } - - if (fblocks.hasIndex(fdepth)) { - h.setRaw(xf, i, zf, fblocks.get(fdepth)); - continue; - } - - h.setRaw(xf, i, zf, context.getFluid().get(xf, zf)); - continue; - } - - if (i <= he) { - depth = he - i; - if (blocks == null) { - blocks = biome.generateLayers(getDimension(), realX, realZ, rng, - he, - he, - getData(), - getComplex()); - } - - - if (blocks.hasIndex(depth)) { - h.setRaw(xf, i, zf, blocks.get(depth)); - continue; - } - - ore = biome.generateOres(realX, i, realZ, rng, getData(), false); - ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), false) : ore; - ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore; - - if (ore != null) { - h.setRaw(xf, i, zf, ore); - } else { - h.setRaw(xf, i, zf, context.getRock().get(xf, zf)); - } - } - } - - UpperDimensionContext upperContext = getEngine().getUpperContext(); - if (upperContext != null) { - int chunkHeight = h.getHeight(); - boolean bedrockEnabled = getDimension().isBedrock(); - int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ); - int upperGap = getDimension().getUpperDimensionGap(); - int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap); - - if (upperSurfaceY < chunkHeight - 1) { - IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ); - PlatformBlockState upperRock = upperContext.getRockBlock(realX, realZ); - int upperThickness = chunkHeight - 1 - upperSurfaceY; - KList upperBlocks = upperBiome != null - ? upperBiome.generateLayers(upperContext.getDimension(), - realX, realZ, rng, upperThickness, upperThickness, - upperContext.getData(), getComplex()) - : null; - - for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) { - if (y == chunkHeight - 1 && bedrockEnabled) { - h.setRaw(xf, y, zf, BEDROCK); - continue; - } - int depthFromFace = y - upperSurfaceY; - if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) { - h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace)); - } else { - h.setRaw(xf, y, zf, upperRock); - } - } - } - } - } - } - @BlockCoordinates private void terrainSliverOptimized(int x, int z, int xf, Hunk h, ChunkContext context) { int chunkHeight = h.getHeight(); diff --git a/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java b/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java index d0e421e0b..7855f9a99 100644 --- a/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java +++ b/core/src/main/java/art/arcane/iris/engine/data/cache/AtomicCache.java @@ -21,36 +21,32 @@ package art.arcane.iris.engine.data.cache; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.function.NastySupplier; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; public class AtomicCache { - private transient final AtomicReference t; - private transient final AtomicBoolean set; - private transient final ReentrantLock lock; + private static final Object NULL_VALUE = new Object(); + private transient final Object initLock = new Object(); private transient final boolean nullSupport; + private transient volatile Object value; public AtomicCache() { this(false); } public AtomicCache(boolean nullSupport) { - set = nullSupport ? new AtomicBoolean() : null; - t = new AtomicReference<>(); - lock = new ReentrantLock(); this.nullSupport = nullSupport; } public void reset() { - t.set(null); - - if (nullSupport) { - set.set(false); + synchronized (initLock) { + value = null; } } + public T getIfPresent() { + return unwrap(value); + } + public T aquireNasty(NastySupplier t) { return aquire(() -> { try { @@ -73,35 +69,41 @@ public class AtomicCache { } public T aquire(Supplier t) { - if (this.t.get() != null) { - return this.t.get(); - } else if (nullSupport && set.get()) { - return null; + Object v = value; + + if (v != null) { + return unwrap(v); } - lock.lock(); + synchronized (initLock) { + v = value; - if (this.t.get() != null) { - lock.unlock(); - return this.t.get(); - } else if (nullSupport && set.get()) { - lock.unlock(); - return null; - } - - try { - this.t.set(t.get()); - - if (nullSupport) { - set.set(true); + if (v != null) { + return unwrap(v); } - } catch (Throwable e) { - IrisLogging.error("Atomic cache failure!"); - e.printStackTrace(); + + try { + T computed = t.get(); + + if (computed != null) { + value = computed; + return computed; + } + + if (nullSupport) { + value = NULL_VALUE; + } + } catch (Throwable e) { + IrisLogging.error("Atomic cache failure!"); + e.printStackTrace(); + } + + return null; } + } - lock.unlock(); - - return this.t.get(); + @SuppressWarnings("unchecked") + private T unwrap(Object v) { + return v == null || v == NULL_VALUE ? null : (T) v; } } diff --git a/core/src/main/java/art/arcane/iris/engine/data/cache/Multicache.java b/core/src/main/java/art/arcane/iris/engine/data/cache/Multicache.java deleted file mode 100644 index 45bc8188c..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/cache/Multicache.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.cache; - -public interface Multicache { - Cache getCache(int id); - - Cache createCache(); -} - \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java b/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java index 5bf5fec02..603c89152 100644 --- a/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java +++ b/core/src/main/java/art/arcane/iris/engine/data/chunk/LinkedTerrainChunk.java @@ -61,6 +61,21 @@ public class LinkedTerrainChunk implements TerrainChunk { biomes[biomeIndex(x, y, z)] = (Biome) bio.nativeHandle(); } + /** + * Writes the whole vertical column in one pass, striding the flat biome array directly. Equivalent to + * calling setBiome for every y in [minHeight, maxHeight). + */ + public void fillBiomeColumn(int x, int z, PlatformBiome bio) { + Biome handle = (Biome) bio.nativeHandle(); + int stride = CHUNK_SIZE * CHUNK_SIZE; + int index = (z & (CHUNK_SIZE - 1)) * CHUNK_SIZE + (x & (CHUNK_SIZE - 1)); + + for (int y = 0; y < biomeHeight; y++) { + biomes[index] = handle; + index += stride; + } + } + @Override public int getMinHeight() { return minHeight; diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java deleted file mode 100644 index 3a29978aa..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/Deserializer.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; - -public interface Deserializer { - - T fromStream(InputStream stream) throws IOException; - - default T fromFile(File file) throws IOException { - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { - return fromStream(bis); - } - } - - default T fromBytes(byte[] data) throws IOException { - ByteArrayInputStream stream = new ByteArrayInputStream(data); - return fromStream(stream); - } - - default T fromResource(Class clazz, String path) throws IOException { - try (InputStream stream = clazz.getClassLoader().getResourceAsStream(path)) { - if (stream == null) { - throw new IOException("resource \"" + path + "\" not found"); - } - return fromStream(stream); - } - } - - default T fromURL(URL url) throws IOException { - try (InputStream stream = url.openStream()) { - return fromStream(stream); - } - } - - -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/ExceptionBiFunction.java b/core/src/main/java/art/arcane/iris/engine/data/io/ExceptionBiFunction.java deleted file mode 100644 index 2c8f21dc6..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/ExceptionBiFunction.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -@FunctionalInterface -public interface ExceptionBiFunction { - - R accept(T t, U u) throws E; -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/ExceptionTriConsumer.java b/core/src/main/java/art/arcane/iris/engine/data/io/ExceptionTriConsumer.java deleted file mode 100644 index a5204dc06..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/ExceptionTriConsumer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -@FunctionalInterface -public interface ExceptionTriConsumer { - - void accept(T t, U u, V v) throws E; -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthIO.java b/core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthIO.java deleted file mode 100644 index 20903ca5d..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthIO.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -public interface MaxDepthIO { - - default int decrementMaxDepth(int maxDepth) { - if (maxDepth < 0) { - throw new IllegalArgumentException("negative maximum depth is not allowed"); - } else if (maxDepth == 0) { - throw new MaxDepthReachedException("reached maximum depth of NBT structure"); - } - return --maxDepth; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthReachedException.java b/core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthReachedException.java deleted file mode 100644 index 956d67482..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/MaxDepthReachedException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -/** - * Exception indicating that the maximum (de-)serialization depth has been reached. - */ -@SuppressWarnings("serial") -public class MaxDepthReachedException extends RuntimeException { - - public MaxDepthReachedException(String msg) { - super(msg); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java deleted file mode 100644 index 9ea42a380..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/Serializer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -public interface Serializer { - - void toStream(T object, OutputStream out) throws IOException; - - default void toFile(T object, File file) throws IOException { - try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) { - toStream(object, bos); - } - } - - default byte[] toBytes(T object) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - toStream(object, bos); - bos.close(); - return bos.toByteArray(); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java deleted file mode 100644 index 043a447b0..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/StringDeserializer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; - -public interface StringDeserializer extends Deserializer { - - T fromReader(Reader reader) throws IOException; - - default T fromString(String s) throws IOException { - return fromReader(new StringReader(s)); - } - - @Override - default T fromStream(InputStream stream) throws IOException { - try (Reader reader = new InputStreamReader(stream)) { - return fromReader(reader); - } - } - - @Override - default T fromFile(File file) throws IOException { - try (Reader reader = new FileReader(file)) { - return fromReader(reader); - } - } - - @Override - default T fromBytes(byte[] data) throws IOException { - return fromReader(new StringReader(new String(data))); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java b/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java deleted file mode 100644 index eb5608b29..000000000 --- a/core/src/main/java/art/arcane/iris/engine/data/io/StringSerializer.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.data.io; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.StringWriter; -import java.io.Writer; - -public interface StringSerializer extends Serializer { - - void toWriter(T object, Writer writer) throws IOException; - - default String toString(T object) throws IOException { - Writer writer = new StringWriter(); - toWriter(object, writer); - writer.flush(); - return writer.toString(); - } - - @Override - default void toStream(T object, OutputStream stream) throws IOException { - Writer writer = new OutputStreamWriter(stream); - toWriter(object, writer); - writer.flush(); - } - - @Override - default void toFile(T object, File file) throws IOException { - try (Writer writer = new FileWriter(file)) { - toWriter(object, writer); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java index 0c139bda8..74197c727 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/DecoratorCore.java @@ -166,12 +166,10 @@ final class DecoratorCore { } if (!decorator.isForcePlace()) { - if (decorator.getWhitelist() != null - && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(irisData).equals(bdx))) { + if (decorator.getWhitelist() != null && !matchesPalette(decorator.getWhitelistArray(irisData), bdx)) { return; } - if (decorator.getBlacklist() != null - && decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(irisData).equals(bdx))) { + if (decorator.getBlacklist() != null && matchesPalette(decorator.getBlacklistArray(irisData), bdx)) { return; } } @@ -203,6 +201,16 @@ final class DecoratorCore { } } + private static boolean matchesPalette(PlatformBlockState[] palette, PlatformBlockState surface) { + for (int i = 0; i < palette.length; i++) { + if (palette[i].equals(surface)) { + return true; + } + } + + return false; + } + static void placeSingleAt(IrisDecorator decorator, int x, int z, int realX, int height, int realZ, Hunk data, RNG rng, IrisData irisData, boolean applyFixFaces, EngineMantle mantle) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedBiModifier.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedBiModifier.java deleted file mode 100644 index 5f53de922..000000000 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedBiModifier.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.framework; - - -import art.arcane.iris.util.project.hunk.Hunk; -public abstract class EngineAssignedBiModifier extends EngineAssignedComponent implements EngineBiModifier { - public EngineAssignedBiModifier(Engine engine, String name) { - super(engine, name); - } - - public abstract void onModify(int x, int z, Hunk a, Hunk b); - - @Override - public void modify(int x, int z, Hunk a, Hunk b) { - onModify(x, z, a, b); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java index 40334dd46..a2ba49676 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedModifier.java @@ -37,7 +37,19 @@ public abstract class EngineAssignedModifier extends EngineAssignedComponent onModify(x, z, output, multicore, context); } catch (Throwable e) { IrisLogging.error("Modifier Failure: " + getName()); - e.printStackTrace(); + IrisLogging.reportError(e); + + // A failed modifier leaves a half-written chunk. Never continue as if it succeeded, + // let the chunk generation failure path handle it. + if (e instanceof Error error) { + throw error; + } + + if (e instanceof RuntimeException runtime) { + throw runtime; + } + + throw new IllegalStateException("Modifier Failure: " + getName(), e); } } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineBiModifier.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineBiModifier.java deleted file mode 100644 index d6eb6dc32..000000000 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineBiModifier.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.framework; - - -import art.arcane.iris.util.project.hunk.Hunk; -public interface EngineBiModifier extends EngineComponent { - void modify(int x, int z, Hunk a, Hunk b); -} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineData.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineData.java deleted file mode 100644 index 5f67d4e88..000000000 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineData.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.framework; - -import art.arcane.iris.spi.IrisLogging; -import com.google.gson.Gson; -import art.arcane.iris.engine.object.IrisPosition; -import art.arcane.volmlib.util.io.IO; -import lombok.Data; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -@Data -public class EngineData { - private String dimension; - private String lastVersion; - private List strongholdPositions; - - public static EngineData load(File f) { - try { - f.getParentFile().mkdirs(); - return new Gson().fromJson(IO.readAll(f), EngineData.class); - } catch (Throwable e) { - IrisLogging.reportError(e); - - } - - return new EngineData(); - } - - public void save(File f) { - try { - f.getParentFile().mkdirs(); - IO.writeAll(f, new Gson().toJson(this)); - } catch (IOException e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/PregeneratedData.java b/core/src/main/java/art/arcane/iris/engine/framework/PregeneratedData.java deleted file mode 100644 index 48d1fc831..000000000 --- a/core/src/main/java/art/arcane/iris/engine/framework/PregeneratedData.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.framework; - -import art.arcane.iris.engine.data.chunk.TerrainChunk; -import art.arcane.iris.spi.PlatformBiome; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.util.common.data.B; -import art.arcane.iris.util.project.hunk.Hunk; -import lombok.Data; - -import java.util.concurrent.atomic.AtomicBoolean; - -@Data -public class PregeneratedData { - private final Hunk blocks; - private final Hunk post; - private final Hunk biomes; - private final AtomicBoolean postMod; - - public PregeneratedData(int height) { - postMod = new AtomicBoolean(false); - blocks = Hunk.newAtomicHunk(16, height, 16); - biomes = Hunk.newAtomicHunk(16, height, 16); - Hunk p = Hunk.newMappedHunkSynced(16, height, 16); - post = p.trackWrite(postMod); - } - - public Runnable inject(TerrainChunk tc) { - blocks.iterateSync((x, y, z, b) -> { - if (b != null) { - tc.setBlock(x, y, z, b); - } - - PlatformBiome bf = biomes.get(x, y, z); - if (bf != null) { - tc.setBiome(x, y, z, bf); - } - }); - - if (postMod.get()) { - return () -> Hunk.view(tc).insertSoftly(0, 0, 0, post, (b) -> b == null || B.isAirOrFluid(b)); - } - - return () -> { - }; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/framework/ResultLocator.java b/core/src/main/java/art/arcane/iris/engine/framework/ResultLocator.java deleted file mode 100644 index 0b3379ba2..000000000 --- a/core/src/main/java/art/arcane/iris/engine/framework/ResultLocator.java +++ /dev/null @@ -1,112 +0,0 @@ -package art.arcane.iris.engine.framework; - -import art.arcane.iris.core.IrisSettings; -import art.arcane.iris.engine.object.IrisObject; -import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.Spiraler; -import art.arcane.iris.util.common.parallel.BurstExecutor; -import art.arcane.iris.util.common.parallel.MultiBurst; -import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import org.apache.commons.lang3.function.TriFunction; - -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -@FunctionalInterface -public interface ResultLocator { - static void cancelSearch() { - if (LocatorCanceller.cancel != null) { - LocatorCanceller.cancel.run(); - LocatorCanceller.cancel = null; - } - } - - static ResultLocator locateObject(Collection keys) { - return (e, pos) -> { - Set objects = e.getObjectsAt(pos.getX(), pos.getZ()); - for (String object : objects) { - if (!keys.contains(object)) continue; - return e.getData().getObjectLoader().load(object); - } - return null; - }; - } - - T find(Engine e, Position2 chunkPos); - - default ResultLocator then(TriFunction filter) { - return (e, pos) -> { - var t = find(e, pos); - return t != null ? filter.apply(e, pos, t) : null; - }; - } - - default Future> find(Engine engine, Position2 pos, long timeout, Consumer checks, boolean cancelable) throws WrongEngineBroException { - if (engine.isClosed()) { - throw new WrongEngineBroException(); - } - - cancelSearch(); - - return MultiBurst.burst.completeValue(() -> { - int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 17; - MultiBurst burst = MultiBurst.burst; - AtomicBoolean found = new AtomicBoolean(false); - AtomicReference> foundObj = new AtomicReference<>(); - Position2 cursor = pos; - AtomicInteger searched = new AtomicInteger(); - AtomicBoolean stop = new AtomicBoolean(false); - PrecisionStopwatch px = PrecisionStopwatch.start(); - if (cancelable) LocatorCanceller.cancel = () -> stop.set(true); - AtomicReference next = new AtomicReference<>(cursor); - Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z))); - s.setOffset(cursor.getX(), cursor.getZ()); - s.next(); - while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) { - BurstExecutor e = burst.burst(tc); - - for (int i = 0; i < tc; i++) { - Position2 p = next.get(); - s.next(); - e.queue(() -> { - var o = find(engine, p); - if (o != null) { - if (foundObj.get() == null) { - foundObj.set(new Result<>(o, p)); - } - - found.set(true); - } - searched.incrementAndGet(); - }); - } - - e.complete(); - checks.accept(searched.get()); - } - - LocatorCanceller.cancel = null; - - if (found.get() && foundObj.get() != null) { - return foundObj.get(); - } - - return null; - }); - } - - record Result(T obj, Position2 pos) { - public int getBlockX() { - return (pos.getX() << 4) + 8; - } - - public int getBlockZ() { - return (pos.getZ() << 4) + 8; - } - } -} 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 fa5a51ad8..a0efebda2 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 @@ -229,23 +229,6 @@ public interface EngineMantle extends MatterGenerator { getMantle().set(x, y, z, UpdateMatter.ON); } - @BlockCoordinates - default void dropCavernBlock(int x, int y, int z) { - Matter matter = getMantle().getChunk(x & 15, z & 15).get(y & 15); - - if (matter != null) { - matter.slice(MatterCavern.class).set(x & 15, y & 15, z & 15, null); - } - } - - default boolean queueRegenerate(int x, int z) { - return false; // TODO: - } - - default boolean dequeueRegenerate(int x, int z) { - return false;// TODO: - } - default int getLoadedRegionCount() { return getMantle().getLoadedRegionCount(); } 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 1af910946..bccb86322 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 @@ -26,13 +26,11 @@ import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.tools.WorldMaintenance; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IObjectPlacer; import art.arcane.iris.engine.object.IrisGeneratorStyle; import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.TileData; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.function.Function3; @@ -44,17 +42,15 @@ import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.common.scheduling.J; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Data; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.B; import org.bukkit.util.Vector; import java.util.HashSet; -import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicReferenceArray; import static art.arcane.iris.engine.mantle.EngineMantle.AIR; @@ -62,19 +58,22 @@ import static art.arcane.iris.engine.mantle.EngineMantle.AIR; public class MantleWriter implements IObjectPlacer, AutoCloseable { private final EngineMantle engineMantle; private final Mantle mantle; - private final Map> cachedChunks; private final int radius; private final int x; private final int z; + private final int windowSide; + private final AtomicReferenceArray> window; public MantleWriter(EngineMantle engineMantle, Mantle mantle, int x, int z, int radius, boolean multicore) { this.engineMantle = engineMantle; this.mantle = mantle; this.radius = radius * 2; - final int d = this.radius + 1; - this.cachedChunks = multicore ? new KMap<>(d * d, 0.75f, Math.max(32, Runtime.getRuntime().availableProcessors() * 4)) : new Long2ObjectOpenHashMap<>(d * d); this.x = x; this.z = z; + // Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the + // boxed per-block map lookup on the placement and carve hot paths. + this.windowSide = (this.radius * 2) + 1; + this.window = new AtomicReferenceArray<>(windowSide * windowSide); final boolean foliaMaintenance = J.isFolia() && WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity()); @@ -82,18 +81,14 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { if (foliaMaintenance && IrisSettings.get().getGeneral().isDebug()) { IrisLogging.info("MantleWriter using sequential chunk prefetch for maintenance regen at " + x + "," + z + "."); } - Map> map = multicore - ? cachedChunks - : new KMap>(d * d, 1f, parallelism); mantle.getChunks( x - radius, x + radius, z - radius, z + radius, parallelism, - (i, j, c) -> map.put(Cache.key(i, j), c.use()) + this::storePrefetchedChunk ); - if (!multicore) cachedChunks.putAll(map); } private static Set getBallooned(Set vset, double radius) { @@ -337,19 +332,48 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { @ChunkCoordinates public MantleChunk acquireChunk(int cx, int cz) { - if (cx < this.x - radius || cx > this.x + radius - || cz < this.z - radius || cz > this.z + radius) { + int index = windowIndex(cx, cz); + if (index < 0) { IrisLogging.error("Mantle Writer Accessed chunk out of bounds" + cx + "," + cz); return null; } - final Long key = Cache.key(cx, cz); - MantleChunk chunk = cachedChunks.get(key); - if (chunk == null) { - chunk = mantle.getChunk(cx, cz).use(); - MantleChunk old = cachedChunks.put(key, chunk); - if (old != null) old.release(); + + while (true) { + MantleChunk chunk = window.get(index); + if (chunk != null) { + return chunk; + } + + // Losing this race must release our own use, never the winner's: the winner is already + // writing through the chunk it published. + MantleChunk acquired = mantle.getChunk(cx, cz).use(); + if (window.compareAndSet(index, null, acquired)) { + return acquired; + } + acquired.release(); + } + } + + @ChunkCoordinates + private int windowIndex(int cx, int cz) { + int localX = cx - x + radius; + int localZ = cz - z + radius; + if (localX < 0 || localX >= windowSide || localZ < 0 || localZ >= windowSide) { + return -1; + } + return (localX * windowSide) + localZ; + } + + @ChunkCoordinates + private void storePrefetchedChunk(int cx, int cz, MantleChunk chunk) { + int index = windowIndex(cx, cz); + if (index < 0) { + return; + } + + if (!window.compareAndSet(index, null, chunk.use())) { + chunk.release(); } - return chunk; } @Override @@ -934,10 +958,11 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { @Override public void close() { - Iterator> iterator = cachedChunks.values().iterator(); - while (iterator.hasNext()) { - iterator.next().release(); - iterator.remove(); + for (int index = 0; index < window.length(); index++) { + MantleChunk chunk = window.getAndSet(index, null); + if (chunk != null) { + chunk.release(); + } } } } 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 4a6109d71..907dbea12 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 @@ -1,6 +1,7 @@ package art.arcane.iris.engine.mantle; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.ChunkCoordinates; @@ -13,11 +14,18 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; public interface MatterGenerator { MultiBurst DISPATCHER = MultiBurst.burst; ConcurrentHashMap> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>(); + long COMPONENT_TASK_POLL_MS = 1000L; + long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L); Engine getEngine(); @@ -39,137 +47,146 @@ public interface MatterGenerator { LongOpenHashSet partialChunks = new LongOpenHashSet(); try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) { - for (MantlePass pass : getComponents()) { - int passRadius = pass.passChunkRadius(); - List passComponents = pass.components(); - MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()]; - int[] componentPassRadii = new int[passComponents.size()]; - int enabledComponentCount = 0; - for (MantleComponent component : passComponents) { - if (component.isEnabled()) { - // 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.getRadius() + pass.downstreamBlockRadius(); - componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0; - enabledComponents[enabledComponentCount++] = component; + // Every task launched below writes through this writer. They must all be finished before + // close() releases the cached chunks, even when a pass throws, or detached pool threads + // write into released chunks. + List outstandingTasks = null; + + try { + for (MantlePass pass : getComponents()) { + int passRadius = pass.passChunkRadius(); + List passComponents = pass.components(); + MantleComponent[] enabledComponents = new MantleComponent[passComponents.size()]; + int[] componentPassRadii = new int[passComponents.size()]; + int enabledComponentCount = 0; + for (MantleComponent component : passComponents) { + if (component.isEnabled()) { + // 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.getRadius() + pass.downstreamBlockRadius(); + componentPassRadii[enabledComponentCount] = componentReach > 0 ? Math.ceilDiv(componentReach, 16) : 0; + enabledComponents[enabledComponentCount++] = component; + } } - } - if (enabledComponentCount == 0) { - continue; - } + if (enabledComponentCount == 0) { + continue; + } - boolean inlineComponents = multicore && DISPATCHER.ownsCurrentThread(); - List> launchedTasks = multicore && !inlineComponents ? new ArrayList<>() : null; - MantleComponent[] eligibleComponents = new MantleComponent[enabledComponentCount]; + // A dispatcher thread runs its components inline: dispatching from inside the pool + // only trades a worker for a blocked worker. + boolean asyncComponents = multicore && !DISPATCHER.ownsCurrentThread(); + if (asyncComponents && outstandingTasks == null) { + outstandingTasks = new ArrayList<>(); + } + MantleComponent[] eligibleComponents = new MantleComponent[enabledComponentCount]; - for (int i = -passRadius; i <= passRadius; i++) { - int absI = Math.abs(i); - for (int j = -passRadius; j <= passRadius; j++) { - int absJ = Math.abs(j); - int passX = x + i; - int passZ = z + j; - long passKey = chunkKey(passX, passZ); - boolean partial = false; - boolean anyComponentInRadius = false; + for (int i = -passRadius; i <= passRadius; i++) { + int absI = Math.abs(i); + for (int j = -passRadius; j <= passRadius; j++) { + int absJ = Math.abs(j); + int passX = x + i; + int passZ = z + j; + long passKey = chunkKey(passX, passZ); + boolean partial = false; + boolean anyComponentInRadius = false; - for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { - int componentPassRadius = componentPassRadii[componentIndex]; - if (absI > componentPassRadius || absJ > componentPassRadius) { - partial = true; - } else { - anyComponentInRadius = true; - } - } - - if (!anyComponentInRadius) { - partialChunks.add(passKey); - continue; - } - - if (partial) { - partialChunks.add(passKey); - } - - MantleChunk chunk = writer.acquireChunk(passX, passZ); - if (chunk.isFlagged(MantleFlag.PLANNED)) { - continue; - } - - int eligibleComponentCount = 0; - for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { - MantleComponent component = enabledComponents[componentIndex]; - int componentPassRadius = componentPassRadii[componentIndex]; - if (absI > componentPassRadius || absJ > componentPassRadius) { - continue; - } - - if (chunk.isFlagged(component.getFlag())) { - continue; - } - - MantleFlag[] prerequisites = component.getPrerequisiteFlags(); - if (prerequisites.length > 0) { - boolean prerequisitesMet = true; - for (MantleFlag prereq : prerequisites) { - if (!chunk.isFlagged(prereq)) { - prerequisitesMet = false; - break; - } + for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { + int componentPassRadius = componentPassRadii[componentIndex]; + if (absI > componentPassRadius || absJ > componentPassRadius) { + partial = true; + } else { + anyComponentInRadius = true; } - if (!prerequisitesMet) { - partialChunks.add(passKey); + } + + if (!anyComponentInRadius) { + partialChunks.add(passKey); + continue; + } + + if (partial) { + partialChunks.add(passKey); + } + + MantleChunk chunk = writer.acquireChunk(passX, passZ); + if (chunk == null) { + throw new IllegalStateException("Mantle pass chunk " + passX + "," + passZ + + " is outside the writer prepared at " + x + "," + z + " with radius " + writeRadius); + } + + if (chunk.isFlagged(MantleFlag.PLANNED)) { + continue; + } + + int eligibleComponentCount = 0; + for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) { + MantleComponent component = enabledComponents[componentIndex]; + int componentPassRadius = componentPassRadii[componentIndex]; + if (absI > componentPassRadius || absJ > componentPassRadius) { continue; } + + if (chunk.isFlagged(component.getFlag())) { + continue; + } + + MantleFlag[] prerequisites = component.getPrerequisiteFlags(); + if (prerequisites.length > 0) { + boolean prerequisitesMet = true; + for (MantleFlag prereq : prerequisites) { + if (!chunk.isFlagged(prereq)) { + prerequisitesMet = false; + break; + } + } + if (!prerequisitesMet) { + partialChunks.add(passKey); + continue; + } + } + + eligibleComponents[eligibleComponentCount++] = component; } - eligibleComponents[eligibleComponentCount++] = component; - } + if (eligibleComponentCount == 0) { + continue; + } - if (eligibleComponentCount == 0) { - continue; - } - - int finalPassX = passX; - int finalPassZ = passZ; - if (multicore) { - if (inlineComponents) { + int finalPassX = passX; + int finalPassZ = passZ; + if (asyncComponents) { + for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) { + MantleComponent component = eligibleComponents[componentIndex]; + outstandingTasks.add(runComponentAsync(chunk, component, writer, finalPassX, finalPassZ, context)); + } + } else { for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) { MantleComponent component = eligibleComponents[componentIndex]; runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context); } - } else { - for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) { - MantleComponent component = eligibleComponents[componentIndex]; - launchedTasks.add(runComponentAsync(chunk, component, writer, finalPassX, finalPassZ, context)); - } - } - } else { - for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) { - MantleComponent component = eligibleComponents[componentIndex]; - runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context); } } } - } - if (launchedTasks != null) { - for (CompletableFuture launchedTask : launchedTasks) { - launchedTask.join(); + if (asyncComponents) { + awaitComponentTasks(outstandingTasks); } } - } - for (int i = -getRealRadius(); i <= getRealRadius(); i++) { - for (int j = -getRealRadius(); j <= getRealRadius(); j++) { - int realX = x + i; - int realZ = z + j; - long realKey = chunkKey(realX, realZ); - if (partialChunks.contains(realKey)) { - continue; + for (int i = -getRealRadius(); i <= getRealRadius(); i++) { + for (int j = -getRealRadius(); j <= getRealRadius(); j++) { + int realX = x + i; + int realZ = z + j; + long realKey = chunkKey(realX, realZ); + if (partialChunks.contains(realKey)) { + continue; + } + writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true); } - writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true); } + } finally { + abandonComponentTasks(outstandingTasks); } } } @@ -178,7 +195,7 @@ public interface MatterGenerator { return (((long) x) << 32) ^ (z & 0xffffffffL); } - private CompletableFuture runComponentAsync( + private MatterComponentTask runComponentAsync( MantleChunk chunk, MantleComponent component, MantleWriter writer, @@ -187,30 +204,121 @@ public interface MatterGenerator { ChunkContext context ) { MantleFlag flag = component.getFlag(); + MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag); if (chunk.isFlagged(flag)) { - return CompletableFuture.completedFuture(null); + return new MatterComponentTask(key, CompletableFuture.completedFuture(null), null); } - MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag.ordinal()); CompletableFuture future = new CompletableFuture<>(); CompletableFuture existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, future); if (existing != null) { - return existing; + return new MatterComponentTask(key, existing, null); } try { if (DISPATCHER.ownsCurrentThread()) { completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context); - } else { - CompletableFuture.runAsync(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context), DISPATCHER); + return new MatterComponentTask(key, future, null); } + + Future submission = DISPATCHER.submit(() -> completeComponentTask(future, key, chunk, component, writer, chunkX, chunkZ, context)); + return new MatterComponentTask(key, future, submission); } catch (Throwable throwable) { IN_FLIGHT_COMPONENTS.remove(key, future); future.completeExceptionally(throwable); throw throwable; } + } - return future; + /** + * Pass barrier. Waits for every launched task, even if one of them failed, so that no task is + * still writing through the writer once this returns. The first failure is rethrown. + */ + private static void awaitComponentTasks(List tasks) { + if (tasks.isEmpty()) { + return; + } + + Throwable failure = null; + for (MatterComponentTask task : tasks) { + try { + awaitComponentTask(task); + } catch (Throwable throwable) { + if (failure == null) { + failure = throwable; + } else if (failure != throwable) { + failure.addSuppressed(throwable); + } + } + } + tasks.clear(); + + if (failure != null) { + throw failure instanceof RuntimeException runtime ? runtime : new CompletionException(failure); + } + } + + /** + * Drains tasks that never reached their pass barrier because the pass threw. The generation + * attempt is already failing, so the wait itself is the point and failures are dropped rather + * than masking the original throwable. + */ + private static void abandonComponentTasks(List tasks) { + if (tasks == null || tasks.isEmpty()) { + return; + } + + try { + awaitComponentTasks(tasks); + } catch (Throwable ignored) { + } + } + + private static void awaitComponentTask(MatterComponentTask task) { + CompletableFuture future = task.future(); + long start = System.currentTimeMillis(); + boolean interrupted = false; + + try { + while (true) { + try { + future.get(COMPONENT_TASK_POLL_MS, TimeUnit.MILLISECONDS); + return; + } catch (InterruptedException interruption) { + // The writer cannot close while this task still writes through it, so the wait is + // uninterruptible and the flag is restored on the way out. + interrupted = true; + } catch (TimeoutException timeout) { + Future submission = task.submission(); + boolean dropped = submission != null && submission.isDone(); + if (future.isDone()) { + continue; + } + + long waited = System.currentTimeMillis() - start; + if (!dropped && waited < COMPONENT_TASK_TIMEOUT_MS) { + continue; + } + + // The task can no longer complete (the dispatcher dropped it) or is wedged. Drop + // the dedup entry so later generations do not inherit a future that never + // completes, and release anything else already waiting on it. + IllegalStateException failure = new IllegalStateException("Mantle component " + task.key() + + (dropped ? " was dropped by the dispatcher" : " did not complete in " + waited + "ms")); + IN_FLIGHT_COMPONENTS.remove(task.key(), future); + future.completeExceptionally(failure); + IrisLogging.error(failure.getMessage()); + throw new CompletionException(failure); + } catch (ExecutionException executionFailure) { + Throwable cause = executionFailure.getCause(); + throw new CompletionException(cause != null ? cause : executionFailure); + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } } private void completeComponentTask( @@ -245,17 +353,24 @@ public interface MatterGenerator { chunk.raiseFlagSuspend(component.getFlag(), () -> component.generateLayer(writer, chunkX, chunkZ, context)); } + /** + * A launched component task. {@code submission} is the dispatcher handle when this generation + * owns the task, and null when the future belongs to another generation or already completed. + */ + record MatterComponentTask(MatterTaskKey key, CompletableFuture future, Future submission) { + } + final class MatterTaskKey { private final Mantle mantle; private final int chunkX; private final int chunkZ; - private final int flagOrdinal; + private final MantleFlag flag; - MatterTaskKey(Mantle mantle, int chunkX, int chunkZ, int flagOrdinal) { + MatterTaskKey(Mantle mantle, int chunkX, int chunkZ, MantleFlag flag) { this.mantle = mantle; this.chunkX = chunkX; this.chunkZ = chunkZ; - this.flagOrdinal = flagOrdinal; + this.flag = flag; } @Override @@ -271,7 +386,7 @@ public interface MatterGenerator { return mantle == other.mantle && chunkX == other.chunkX && chunkZ == other.chunkZ - && flagOrdinal == other.flagOrdinal; + && flag.ordinal() == other.flag.ordinal(); } @Override @@ -279,8 +394,13 @@ public interface MatterGenerator { int result = System.identityHashCode(mantle); result = 31 * result + chunkX; result = 31 * result + chunkZ; - result = 31 * result + flagOrdinal; + result = 31 * result + flag.ordinal(); return result; } + + @Override + public String toString() { + return flag.name() + " at " + chunkX + "," + chunkZ; + } } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java new file mode 100644 index 000000000..cbc77fe3d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveCarveScratch.java @@ -0,0 +1,71 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; + +final class CaveCarveScratch { + final int[] columnMaxY = new int[256]; + final int[] waterMaxY = new int[256]; + final int[] surfaceBreakFloorY = new int[256]; + final boolean[] surfaceBreakColumn = new boolean[256]; + final double[] columnThreshold = new double[256]; + final double[] passThreshold = new double[256]; + final double[] fullWeights = new double[256]; + final double[] clampedColumnWeights = new double[256]; + final int[] activeColumnIndices = new int[256]; + final int[] activeColumnTopY = new int[256]; + final int[] planeColumnIndices = new int[256]; + final double[] planeThresholdLimit = new double[256]; + final boolean[] planeCarve = new boolean[256]; + final double[] adaptivePlaneDensity = new double[81]; + final double[] adaptivePlanePrediction = new double[256]; + final double[] adaptivePlaneAmbiguity = new double[256]; + final int[] adaptivePlaneSampleBounds = new int[4]; + final int[] adaptiveCellX = new int[256]; + final int[] adaptiveCellZ = new int[256]; + final int[] adaptiveRow0 = new int[256]; + final int[] adaptiveRow1 = new int[256]; + final double[] adaptiveTx = new double[256]; + final double[] adaptiveTz = new double[256]; + final boolean[] warpCacheSet = new boolean[256]; + final int[] warpCacheX = new int[256]; + final int[] warpCacheY = new int[256]; + final int[] warpCacheZ = new int[256]; + final double[] warpCacheA = new double[256]; + final double[] warpCacheB = new double[256]; + final int[] tileIndices = new int[4]; + final int[] tileLocalX = new int[4]; + final int[] tileLocalZ = new int[4]; + final int[] tileTopY = new int[4]; + CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0]; + double[] activeModuleRemainingMin = new double[0]; + double[] activeModuleRemainingMax = new double[0]; + int activeModulesY = Integer.MIN_VALUE; + int activeModuleCount; + double[] verticalEdgeFade = new double[0]; + MatterCavern[] matterByY = new MatterCavern[0]; + Matter[] sectionMatter = new Matter[0]; + MatterSlice[] sectionSlices = new MatterSlice[0]; + int adaptiveGeometryStep = -1; + int adaptiveGeometryAxisCells = -1; + boolean fullWeightsInitialized; +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java new file mode 100644 index 000000000..295edc3e1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveFieldModuleState.java @@ -0,0 +1,58 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.object.IrisCaveFieldModule; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.util.project.noise.CNG; + +final class CaveFieldModuleState { + final CNG density; + final int minY; + final int maxY; + final double weight; + final double threshold; + final boolean invert; + final double minContribution; + final double maxContribution; + + CaveFieldModuleState(IrisCaveFieldModule module, CNG density) { + IrisRange range = module.getVerticalRange(); + this.density = density; + this.minY = (int) Math.floor(range.getMin()); + this.maxY = (int) Math.ceil(range.getMax()); + this.weight = module.getWeight(); + this.threshold = module.getThreshold(); + this.invert = module.isInvert(); + double rawMin = invert ? threshold - 1D : -1D - threshold; + double rawMax = invert ? threshold + 1D : 1D - threshold; + this.minContribution = rawMin * weight; + this.maxContribution = rawMax * weight; + } + + double sample(double x, int y, double z) { + double sampled = density.noiseFastSigned3D(x, y, z); + return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; + } + + double sample(double x, double y, double z) { + double sampled = density.noiseFastSigned3D(x, y, z); + return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveWaterSupportPlan.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveWaterSupportPlan.java new file mode 100644 index 000000000..00f55472c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveWaterSupportPlan.java @@ -0,0 +1,120 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; + +import java.util.BitSet; +import java.util.IdentityHashMap; +import java.util.Map; + +final class CaveWaterSupportPlan { + private final IdentityHashMap groups = new IdentityHashMap<>(); + + void add(int localX, int y, int localZ, MatterCavern water, MatterCavern air) { + WaterCandidateGroup group = groups.computeIfAbsent(water, key -> new WaterCandidateGroup(water, air)); + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + group.positions.set((y << 8) | columnIndex); + } + + void resolve(MantleChunk chunk) { + if (chunk == null) { + groups.clear(); + return; + } + + for (Map.Entry entry : groups.entrySet()) { + WaterCandidateGroup group = entry.getValue(); + for (int position = group.positions.nextSetBit(0); position >= 0; position = group.positions.nextSetBit(position + 1)) { + int y = position >>> 8; + int columnIndex = position & 255; + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + MatterCavern current = getCavern(chunk, localX, y, localZ); + if (current != group.water || hasCupSupport(chunk, localX, y, localZ)) { + continue; + } + + Matter section = chunk.get(y >> 4); + MatterSlice cavernSlice = section.getSlice(MatterCavern.class); + cavernSlice.set(localX, y & 15, localZ, group.air); + } + } + groups.clear(); + } + + private static boolean hasCupSupport(MantleChunk chunk, int localX, int y, int localZ) { + if (localX <= 0 || localX >= 15 || localZ <= 0 || localZ >= 15 + || y <= 1 || !isSolid(chunk, localX, y - 1, localZ) + || !isSolid(chunk, localX, y - 2, localZ)) { + return false; + } + + int support = 0; + if (isSolid(chunk, localX + 1, y, localZ)) { + support++; + } + if (isSolid(chunk, localX - 1, y, localZ)) { + support++; + } + if (isSolid(chunk, localX, y, localZ + 1)) { + support++; + } + if (isSolid(chunk, localX, y, localZ - 1)) { + support++; + } + if (isSolid(chunk, localX, y + 1, localZ)) { + support++; + } + return support >= 4; + } + + private static boolean isSolid(MantleChunk chunk, int localX, int y, int localZ) { + if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16) { + return false; + } + MatterCavern cavern = getCavern(chunk, localX, y, localZ); + return cavern == null || !cavern.isCavern(); + } + + private static MatterCavern getCavern(MantleChunk chunk, int localX, int y, int localZ) { + Matter section = chunk.get(y >> 4); + if (section == null) { + return null; + } + + MatterSlice cavernSlice = section.getSlice(MatterCavern.class); + return cavernSlice == null ? null : cavernSlice.get(localX, y & 15, localZ); + } + + private static final class WaterCandidateGroup { + private final MatterCavern water; + private final MatterCavern air; + private final BitSet positions = new BitSet(); + + private WaterCandidateGroup(MatterCavern water, MatterCavern air) { + this.water = water; + this.air = air; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java new file mode 100644 index 000000000..34fa73ebe --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java @@ -0,0 +1,159 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IObjectPlacer; +import art.arcane.iris.engine.object.TileData; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; + +final class GoldenDebugObjectPlacer implements IObjectPlacer { + private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec()); + private static final boolean GOLDEN_DEBUG = GOLDEN_DEBUG_TARGET != null; + + private final IObjectPlacer delegate; + private final String tag; + + GoldenDebugObjectPlacer(IObjectPlacer delegate, String tag) { + this.delegate = delegate; + this.tag = tag; + } + + private static String resolveGoldenDebugSpec() { + String property = System.getProperty("iris.goldendebug"); + if (property != null && !property.isBlank()) { + return property; + } + try { + java.io.File marker = new java.io.File("plugins/Iris/goldendebug.txt"); + if (marker.isFile()) { + return java.nio.file.Files.readString(marker.toPath()).trim(); + } + } catch (Throwable ignored) { + } + return null; + } + + private static int[] parseGoldenDebugTarget(String raw) { + if (raw == null || raw.isBlank()) { + return null; + } + String[] parts = raw.split(","); + if (parts.length != 2 && parts.length != 3) { + return null; + } + try { + int radius = parts.length == 3 ? Integer.parseInt(parts[2].trim()) : 0; + return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()), radius}; + } catch (NumberFormatException e) { + return null; + } + } + + static boolean isGoldenDebugChunk(int x, int z) { + return GOLDEN_DEBUG + && Math.abs(GOLDEN_DEBUG_TARGET[0] - x) <= GOLDEN_DEBUG_TARGET[2] + && Math.abs(GOLDEN_DEBUG_TARGET[1] - z) <= GOLDEN_DEBUG_TARGET[2]; + } + + @Override + public int getHighest(int x, int z, IrisData data) { + int result = delegate.getHighest(x, z, data); + IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result); + return result; + } + + @Override + public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { + int result = delegate.getHighest(x, z, data, ignoreFluid); + IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result); + return result; + } + + @Override + public void set(int x, int y, int z, PlatformBlockState d) { + delegate.set(x, y, z, d); + } + + @Override + public PlatformBlockState get(int x, int y, int z) { + return delegate.get(x, y, z); + } + + @Override + public boolean isPreventingDecay() { + return delegate.isPreventingDecay(); + } + + @Override + public boolean isCarved(int x, int y, int z) { + boolean result = delegate.isCarved(x, y, z); + IrisLogging.info("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result); + return result; + } + + @Override + public boolean isSurfaceSolid(int x, int y, int z) { + return delegate.isSurfaceSolid(x, y, z); + } + + @Override + public boolean isSolid(int x, int y, int z) { + boolean result = delegate.isSolid(x, y, z); + IrisLogging.info("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result); + return result; + } + + @Override + public boolean isUnderwater(int x, int z) { + return delegate.isUnderwater(x, z); + } + + @Override + public int getFluidHeight() { + return delegate.getFluidHeight(); + } + + @Override + public boolean isDebugSmartBore() { + return delegate.isDebugSmartBore(); + } + + @Override + public void setData(int xx, int yy, int zz, T data) { + delegate.setData(xx, yy, zz, data); + } + + @Override + public T getData(int xx, int yy, int zz, Class t) { + return delegate.getData(xx, yy, zz, t); + } + + @Override + public void setTile(int xx, int yy, int zz, TileData tile) { + delegate.setTile(xx, yy, zz, tile); + } + + @Override + public Engine getEngine() { + return delegate.getEngine(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 20d8c2e18..25856b1a3 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -24,8 +24,10 @@ import art.arcane.iris.engine.mantle.MantleWriter; import art.arcane.iris.engine.object.IrisCaveFieldModule; import art.arcane.iris.engine.object.IrisCaveProfile; import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.engine.object.IrisStyledRange; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; @@ -35,10 +37,7 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.ArrayList; import java.util.Arrays; -import java.util.BitSet; -import java.util.IdentityHashMap; import java.util.List; -import java.util.Map; public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; @@ -59,7 +58,7 @@ public class IrisCaveCarver3D { private final CNG warpDensity; private final CNG surfaceBreakDensity; private final RNG thresholdRng; - private final ModuleState[] modules; + private final CaveFieldModuleState[] modules; private final double inverseNormalization; private final MatterCavern carveAir; private final MatterCavern carveWater; @@ -74,7 +73,12 @@ public class IrisCaveCarver3D { private final boolean hasWarp; private final boolean hasModules; private final int warpResolution; - private final ThreadLocal scratchCache = ThreadLocal.withInitial(Scratch::new); + private final boolean allowWater; + private final boolean waterRequiresFloor; + private final int waterMinDepthBelowSurface; + private final int fluidHeight; + private final int aquiferCeilingY; + private final ThreadLocal scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new); public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { this.engine = engine; @@ -84,7 +88,7 @@ public class IrisCaveCarver3D { this.carveWater = new MatterCavern(true, "", LIQUID_WATER); this.carveLava = new MatterCavern(true, "", LIQUID_LAVA); this.carveForcedAir = new MatterCavern(true, "", LIQUID_FORCED_AIR); - List moduleStates = new ArrayList<>(); + List moduleStates = new ArrayList<>(); RNG baseRng = new RNG(engine.getSeedManager().getCarve()); this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data); @@ -97,18 +101,23 @@ public class IrisCaveCarver3D { this.warpStrength = profile.getWarpStrength(); this.hasWarp = this.warpStrength > 0D; this.warpResolution = 2; + this.allowWater = profile.isAllowWater(); + this.waterRequiresFloor = profile.isWaterRequiresFloor(); + this.waterMinDepthBelowSurface = Math.max(0, profile.getWaterMinDepthBelowSurface()); + this.fluidHeight = engine.getDimension().getFluidHeight(); + this.aquiferCeilingY = engine.getHeight() - 1; double weight = Math.abs(baseWeight) + Math.abs(detailWeight); int index = 0; for (IrisCaveFieldModule module : profile.getModules()) { CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data); - ModuleState state = new ModuleState(module, moduleDensity); + CaveFieldModuleState state = new CaveFieldModuleState(module, moduleDensity); moduleStates.add(state); weight += Math.abs(state.weight); index++; } - this.modules = moduleStates.toArray(new ModuleState[0]); + this.modules = moduleStates.toArray(new CaveFieldModuleState[0]); double normalization = weight <= 0 ? 1 : weight; normalizationFactor = normalization; inverseNormalization = 1D / normalization; @@ -118,7 +127,7 @@ public class IrisCaveCarver3D { } public int carve(MantleWriter writer, int chunkX, int chunkZ) { - Scratch scratch = scratchCache.get(); + CaveCarveScratch scratch = scratchCache.get(); if (!scratch.fullWeightsInitialized) { Arrays.fill(scratch.fullWeights, 1D); scratch.fullWeightsInitialized = true; @@ -173,7 +182,7 @@ public class IrisCaveCarver3D { int[] precomputedSurfaceHeights, IrisRange overrideVerticalRange ) { - WaterSupportPlan waterSupportPlan = new WaterSupportPlan(); + CaveWaterSupportPlan waterSupportPlan = new CaveWaterSupportPlan(); int carved = carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, worldYRange, precomputedSurfaceHeights, overrideVerticalRange, waterSupportPlan); waterSupportPlan.resolve(writer.acquireChunk(chunkX, chunkZ)); @@ -190,11 +199,11 @@ public class IrisCaveCarver3D { IrisRange worldYRange, int[] precomputedSurfaceHeights, IrisRange overrideVerticalRange, - WaterSupportPlan waterSupportPlan + CaveWaterSupportPlan waterSupportPlan ) { PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); try { - Scratch scratch = scratchCache.get(); + CaveCarveScratch scratch = scratchCache.get(); if (columnWeights == null || columnWeights.length < 256) { if (!scratch.fullWeightsInitialized) { Arrays.fill(scratch.fullWeights, 1D); @@ -247,6 +256,23 @@ public class IrisCaveCarver3D { MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY); prepareSectionCaches(scratch, minY, maxY); + // IrisStyledRange.get() resolves its CNG per call, which costs a cache-key allocation, + // a capturing lambda and a shared LRU touch. The CNG is constant for (thresholdRng, data), + // so resolve it once here and replicate the min==max / isFlat short circuits verbatim. + IrisStyledRange densityThreshold = profile.getDensityThreshold(); + double thresholdMin = densityThreshold.getMin(); + double thresholdMax = densityThreshold.getMax(); + double thresholdBias = profile.getThresholdBias(); + CNG thresholdDensity = null; + double constantThreshold = 0D; + if (thresholdMin == thresholdMax) { + constantThreshold = thresholdMin; + } else if (densityThreshold.getStyle().isFlat()) { + constantThreshold = M.lerp(thresholdMin, thresholdMax, 0.5); + } else { + thresholdDensity = densityThreshold.getStyle().create(thresholdRng, data); + } + for (int lx = 0; lx < 16; lx++) { int x = x0 + lx; for (int lz = 0; lz < 16; lz++) { @@ -266,10 +292,14 @@ public class IrisCaveCarver3D { : clearanceTopY; columnMaxY[index] = columnTopY; - waterMaxY[index] = resolveWaterMaxY(columnSurfaceY); + waterMaxY[index] = allowWater + ? Math.min(fluidHeight, columnSurfaceY - waterMinDepthBelowSurface) + : Integer.MIN_VALUE; surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); surfaceBreakColumn[index] = breakColumn; - columnThreshold[index] = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + columnThreshold[index] = (thresholdDensity == null + ? constantThreshold + : thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias; clampedWeights[index] = clampColumnWeight(columnWeights[index]); } } @@ -294,7 +324,7 @@ public class IrisCaveCarver3D { clampedWeights, verticalEdgeFade, matterByY, - profile.isWaterRequiresFloor() ? waterSupportPlan : null, + waterRequiresFloor ? waterSupportPlan : null, resolvedMinWeight, resolvedThresholdPenalty, 0D, @@ -316,7 +346,7 @@ public class IrisCaveCarver3D { clampedWeights, verticalEdgeFade, matterByY, - profile.isWaterRequiresFloor() ? waterSupportPlan : null, + waterRequiresFloor ? waterSupportPlan : null, resolvedMinWeight, resolvedThresholdPenalty, 0D, @@ -341,7 +371,7 @@ public class IrisCaveCarver3D { clampedWeights, verticalEdgeFade, matterByY, - profile.isWaterRequiresFloor() ? waterSupportPlan : null, + waterRequiresFloor ? waterSupportPlan : null, resolvedMinWeight, resolvedThresholdPenalty, 0D, @@ -364,7 +394,7 @@ public class IrisCaveCarver3D { clampedWeights, verticalEdgeFade, matterByY, - profile.isWaterRequiresFloor() ? waterSupportPlan : null, + waterRequiresFloor ? waterSupportPlan : null, resolvedMinWeight, resolvedThresholdPenalty, 0D, @@ -394,14 +424,14 @@ public class IrisCaveCarver3D { double[] clampedWeights, double[] verticalEdgeFade, MatterCavern[] matterByY, - WaterSupportPlan waterSupportPlan, + CaveWaterSupportPlan waterSupportPlan, double minWeight, double thresholdPenalty, double thresholdBoost, boolean skipExistingCarved ) { int carved = 0; - Scratch scratch = scratchCache.get(); + CaveCarveScratch scratch = scratchCache.get(); double[] passThreshold = scratch.passThreshold; int[] activeColumnIndices = scratch.activeColumnIndices; int[] activeColumnTopY = scratch.activeColumnTopY; @@ -457,7 +487,7 @@ public class IrisCaveCarver3D { continue; } - classifyDensityPlane(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); int fadeIndex = y - minY; int localY = y & 15; MatterCavern verticalMatter = matterByY[fadeIndex]; @@ -521,14 +551,14 @@ public class IrisCaveCarver3D { double[] clampedWeights, double[] verticalEdgeFade, MatterCavern[] matterByY, - WaterSupportPlan waterSupportPlan, + CaveWaterSupportPlan waterSupportPlan, double minWeight, double thresholdPenalty, double thresholdBoost, boolean skipExistingCarved ) { int carved = 0; - Scratch scratch = scratchCache.get(); + CaveCarveScratch scratch = scratchCache.get(); double[] passThreshold = scratch.passThreshold; int[] activeColumnIndices = scratch.activeColumnIndices; int[] activeColumnTopY = scratch.activeColumnTopY; @@ -591,6 +621,7 @@ public class IrisCaveCarver3D { effectiveAdaptiveSampleStep ); classifyDensityPlaneAdaptive( + scratch, x0, z0, y, @@ -685,14 +716,14 @@ public class IrisCaveCarver3D { double[] clampedWeights, double[] verticalEdgeFade, MatterCavern[] matterByY, - WaterSupportPlan waterSupportPlan, + CaveWaterSupportPlan waterSupportPlan, double minWeight, double thresholdPenalty, double thresholdBoost, boolean skipExistingCarved ) { int carved = 0; - Scratch scratch = scratchCache.get(); + CaveCarveScratch scratch = scratchCache.get(); double[] passThreshold = scratch.passThreshold; int[] tileIndices = scratch.tileIndices; int[] tileLocalX = scratch.tileLocalX; @@ -767,7 +798,7 @@ public class IrisCaveCarver3D { } for (int y = minY; y <= tileMaxY; y += latticeStep) { - double density = sampleDensityOptimized(x, y, z); + double density = sampleDensityOptimized(scratch, x, y, z); int stampMaxY = Math.min(maxY, y + 1); for (int yy = y; yy <= stampMaxY; yy++) { MatterCavern verticalMatter = matterByY[yy - minY]; @@ -830,14 +861,14 @@ public class IrisCaveCarver3D { double[] clampedWeights, double[] verticalEdgeFade, MatterCavern[] matterByY, - WaterSupportPlan waterSupportPlan, + CaveWaterSupportPlan waterSupportPlan, double minWeight, double thresholdPenalty, double thresholdBoost, boolean skipExistingCarved ) { int carved = 0; - Scratch scratch = scratchCache.get(); + CaveCarveScratch scratch = scratchCache.get(); for (int lx = 0; lx < 16; lx++) { int x = x0 + lx; @@ -865,7 +896,7 @@ public class IrisCaveCarver3D { } localThreshold -= verticalEdgeFade[y - minY]; - if (sampleDensityOptimized(x, y, z) > localThreshold) { + if (sampleDensityOptimized(scratch, x, y, z) > localThreshold) { continue; } @@ -909,41 +940,50 @@ public class IrisCaveCarver3D { } private double sampleDensityOptimized(int x, int y, int z) { + if (!hasWarp && !hasModules) { + return sampleDensityNoWarpNoModules(x, y, z); + } + + return sampleDensityOptimized(scratchCache.get(), x, y, z); + } + + private double sampleDensityOptimized(CaveCarveScratch scratch, int x, int y, int z) { if (!hasWarp) { if (!hasModules) { return sampleDensityNoWarpNoModules(x, y, z); } - return sampleDensityNoWarpModules(x, y, z); + return sampleDensityNoWarpModules(scratch, x, y, z); } if (!hasModules) { - return sampleDensityWarpOnly(x, y, z); + return sampleDensityWarpOnly(scratch, x, y, z); } - return sampleDensityWarpModules(x, y, z); + return sampleDensityWarpModules(scratch, x, y, z); } - private void classifyDensityPlane(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + private void classifyDensityPlane(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { if (!hasWarp) { if (!hasModules) { classifyDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); return; } - classifyDensityPlaneNoWarpModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + classifyDensityPlaneNoWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); return; } if (!hasModules) { - classifyDensityPlaneWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + classifyDensityPlaneWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); return; } - classifyDensityPlaneWarpModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + classifyDensityPlaneWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); } private void classifyDensityPlaneAdaptive( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -955,26 +995,26 @@ public class IrisCaveCarver3D { double adaptiveThresholdMargin ) { if (adaptiveSampleStep <= 1 || planeCount < ADAPTIVE_MIN_PLANE_COLUMNS) { - classifyDensityPlane(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); return; } if (!hasWarp) { if (!hasModules) { - classifyDensityPlaneAdaptiveNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + classifyDensityPlaneAdaptiveNoWarpNoModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); return; } - classifyDensityPlaneAdaptiveNoWarpModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + classifyDensityPlaneAdaptiveNoWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); return; } if (!hasModules) { - classifyDensityPlaneAdaptiveWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + classifyDensityPlaneAdaptiveWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); return; } - classifyDensityPlaneAdaptiveWarpModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + classifyDensityPlaneAdaptiveWarpModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); } private void classifyDensityPlaneNoWarpNoModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { @@ -986,15 +1026,14 @@ public class IrisCaveCarver3D { } } - private void classifyDensityPlaneNoWarpModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { - Scratch scratch = scratchCache.get(); + private void classifyDensityPlaneNoWarpModules(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { int activeModuleCount = prepareActiveModules(scratch, y); if (activeModuleCount == 0) { classifyDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); return; } - ModuleState[] localModules = scratch.activeModules; + CaveFieldModuleState[] localModules = scratch.activeModules; double[] remainingMin = scratch.activeModuleRemainingMin; double[] remainingMax = scratch.activeModuleRemainingMax; @@ -1015,24 +1054,23 @@ public class IrisCaveCarver3D { } } - private void classifyDensityPlaneWarpOnly(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { + private void classifyDensityPlaneWarpOnly(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { int columnIndex = planeColumnIndices[planeIndex]; int x = x0 + PowerOfTwoCoordinates.unpackLocal16X(columnIndex); int z = z0 + (columnIndex & 15); - planeCarve[planeIndex] = classifyDensityPointWarpOnly(x, y, z, planeThresholdLimit[planeIndex]); + planeCarve[planeIndex] = classifyDensityPointWarpOnly(scratch, x, y, z, planeThresholdLimit[planeIndex]); } } - private void classifyDensityPlaneWarpModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { - Scratch scratch = scratchCache.get(); + private void classifyDensityPlaneWarpModules(CaveCarveScratch scratch, int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) { int activeModuleCount = prepareActiveModules(scratch, y); if (activeModuleCount == 0) { - classifyDensityPlaneWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); + classifyDensityPlaneWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve); return; } - ModuleState[] localModules = scratch.activeModules; + CaveFieldModuleState[] localModules = scratch.activeModules; double[] remainingMin = scratch.activeModuleRemainingMin; double[] remainingMax = scratch.activeModuleRemainingMax; @@ -1041,6 +1079,7 @@ public class IrisCaveCarver3D { int x = x0 + PowerOfTwoCoordinates.unpackLocal16X(columnIndex); int z = z0 + (columnIndex & 15); planeCarve[planeIndex] = classifyDensityPointWarpModules( + scratch, x, y, z, @@ -1054,6 +1093,7 @@ public class IrisCaveCarver3D { } private void classifyDensityPlaneAdaptiveNoWarpNoModules( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1064,12 +1104,11 @@ public class IrisCaveCarver3D { int adaptiveSampleStep, double adaptiveThresholdMargin ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; int axisSamples = axisCells + 1; int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; - prepareAdaptivePlaneSampleBounds(planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); int x = x0 + sampleLocalX; @@ -1081,6 +1120,7 @@ public class IrisCaveCarver3D { } classifyAdaptivePlaneColumnsNoWarpNoModules( + scratch, x0, z0, y, @@ -1097,6 +1137,7 @@ public class IrisCaveCarver3D { } private void classifyDensityPlaneAdaptiveNoWarpModules( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1107,21 +1148,20 @@ public class IrisCaveCarver3D { int adaptiveSampleStep, double adaptiveThresholdMargin ) { - Scratch scratch = scratchCache.get(); int activeModuleCount = prepareActiveModules(scratch, y); if (activeModuleCount == 0) { - classifyDensityPlaneAdaptiveNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + classifyDensityPlaneAdaptiveNoWarpNoModules(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); return; } - ModuleState[] localModules = scratch.activeModules; + CaveFieldModuleState[] localModules = scratch.activeModules; double[] remainingMin = scratch.activeModuleRemainingMin; double[] remainingMax = scratch.activeModuleRemainingMax; double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; int axisSamples = axisCells + 1; int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; - prepareAdaptivePlaneSampleBounds(planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); int x = x0 + sampleLocalX; @@ -1133,6 +1173,7 @@ public class IrisCaveCarver3D { } classifyAdaptivePlaneColumnsNoWarpModules( + scratch, x0, z0, y, @@ -1153,6 +1194,7 @@ public class IrisCaveCarver3D { } private void classifyDensityPlaneAdaptiveWarpOnly( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1163,23 +1205,23 @@ public class IrisCaveCarver3D { int adaptiveSampleStep, double adaptiveThresholdMargin ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; int axisSamples = axisCells + 1; int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; - prepareAdaptivePlaneSampleBounds(planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); int x = x0 + sampleLocalX; int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpOnly(x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpOnly(scratch, x, y, z0 + sampleLocalZ); } } classifyAdaptivePlaneColumnsWarpOnly( + scratch, x0, z0, y, @@ -1196,6 +1238,7 @@ public class IrisCaveCarver3D { } private void classifyDensityPlaneAdaptiveWarpModules( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1206,21 +1249,20 @@ public class IrisCaveCarver3D { int adaptiveSampleStep, double adaptiveThresholdMargin ) { - Scratch scratch = scratchCache.get(); int activeModuleCount = prepareActiveModules(scratch, y); if (activeModuleCount == 0) { - classifyDensityPlaneAdaptiveWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); + classifyDensityPlaneAdaptiveWarpOnly(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin); return; } - ModuleState[] localModules = scratch.activeModules; + CaveFieldModuleState[] localModules = scratch.activeModules; double[] remainingMin = scratch.activeModuleRemainingMin; double[] remainingMax = scratch.activeModuleRemainingMax; double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity; int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep; int axisSamples = axisCells + 1; int[] adaptivePlaneSampleBounds = scratch.adaptivePlaneSampleBounds; - prepareAdaptivePlaneSampleBounds(planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); + prepareAdaptivePlaneSampleBounds(scratch, planeColumnIndices, planeCount, adaptiveSampleStep, adaptivePlaneSampleBounds, axisCells); for (int sampleXIndex = adaptivePlaneSampleBounds[0]; sampleXIndex <= adaptivePlaneSampleBounds[1]; sampleXIndex++) { int sampleLocalX = Math.min(sampleXIndex * adaptiveSampleStep, 16); int x = x0 + sampleLocalX; @@ -1239,6 +1281,7 @@ public class IrisCaveCarver3D { } classifyAdaptivePlaneColumnsWarpModulesSampled( + scratch, x0, z0, y, @@ -1259,6 +1302,7 @@ public class IrisCaveCarver3D { } private void classifyAdaptivePlaneColumnsNoWarpNoModules( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1272,10 +1316,10 @@ public class IrisCaveCarver3D { int axisCells, int axisSamples ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; prepareAdaptivePlaneColumns( + scratch, planeColumnIndices, planeCount, adaptiveSampleStep, @@ -1315,6 +1359,7 @@ public class IrisCaveCarver3D { } private void classifyAdaptivePlaneColumnsNoWarpModules( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1327,15 +1372,15 @@ public class IrisCaveCarver3D { double[] adaptivePlaneDensity, int axisCells, int axisSamples, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; prepareAdaptivePlaneColumns( + scratch, planeColumnIndices, planeCount, adaptiveSampleStep, @@ -1396,6 +1441,7 @@ public class IrisCaveCarver3D { } private void classifyAdaptivePlaneColumnsWarpOnly( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1409,10 +1455,10 @@ public class IrisCaveCarver3D { int axisCells, int axisSamples ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; prepareAdaptivePlaneColumns( + scratch, planeColumnIndices, planeCount, adaptiveSampleStep, @@ -1447,11 +1493,12 @@ public class IrisCaveCarver3D { continue; } - planeCarve[planeIndex] = classifyDensityPointWarpOnly(x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); + planeCarve[planeIndex] = classifyDensityPointWarpOnly(scratch, x0 + localX, y, z0 + localZ, planeThresholdLimit[planeIndex]); } } private void classifyAdaptivePlaneColumnsWarpModules( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1464,15 +1511,15 @@ public class IrisCaveCarver3D { double[] adaptivePlaneDensity, int axisCells, int axisSamples, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; prepareAdaptivePlaneColumns( + scratch, planeColumnIndices, planeCount, adaptiveSampleStep, @@ -1494,6 +1541,7 @@ public class IrisCaveCarver3D { double ambiguityMargin = adaptivePlaneAmbiguity[planeIndex]; if (isAdaptivePlaneSampleAligned(localX, localZ, adaptiveSampleStep)) { planeCarve[planeIndex] = classifyDensityPointWarpModulesFromExactDensity( + scratch, x0 + localX, y, z0 + localZ, @@ -1516,6 +1564,7 @@ public class IrisCaveCarver3D { } planeCarve[planeIndex] = classifyDensityPointWarpModules( + scratch, x0 + localX, y, z0 + localZ, @@ -1529,6 +1578,7 @@ public class IrisCaveCarver3D { } private void classifyAdaptivePlaneColumnsWarpModulesSampled( + CaveCarveScratch scratch, int x0, int z0, int y, @@ -1541,15 +1591,15 @@ public class IrisCaveCarver3D { double[] adaptivePlaneDensity, int axisCells, int axisSamples, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax ) { - Scratch scratch = scratchCache.get(); double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction; double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity; prepareAdaptivePlaneColumns( + scratch, planeColumnIndices, planeCount, adaptiveSampleStep, @@ -1585,6 +1635,7 @@ public class IrisCaveCarver3D { } planeCarve[planeIndex] = classifyDensityPointWarpModules( + scratch, x0 + localX, y, z0 + localZ, @@ -1615,7 +1666,7 @@ public class IrisCaveCarver3D { int y, int z, double thresholdLimit, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax @@ -1664,8 +1715,7 @@ public class IrisCaveCarver3D { return Math.floorDiv(c, g) * g; } - private boolean classifyDensityPointWarpOnly(int x, int y, int z, double thresholdLimit) { - Scratch scratch = scratchCache.get(); + private boolean classifyDensityPointWarpOnly(CaveCarveScratch scratch, int x, int y, int z, double thresholdLimit) { int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); @@ -1688,20 +1738,20 @@ public class IrisCaveCarver3D { } private boolean classifyDensityPointWarpModules( + CaveCarveScratch scratch, int x, int y, int z, double thresholdLimit, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax ) { if (activeModuleCount == 0) { - return classifyDensityPointWarpOnly(x, y, z, thresholdLimit); + return classifyDensityPointWarpOnly(scratch, x, y, z, thresholdLimit); } - Scratch scratch = scratchCache.get(); int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); @@ -1746,7 +1796,7 @@ public class IrisCaveCarver3D { int z, double threshold, double density, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax @@ -1778,12 +1828,13 @@ public class IrisCaveCarver3D { } private boolean classifyDensityPointWarpModulesFromExactDensity( + CaveCarveScratch scratch, int x, int y, int z, double threshold, double density, - ModuleState[] localModules, + CaveFieldModuleState[] localModules, int activeModuleCount, double[] remainingMin, double[] remainingMax @@ -1801,7 +1852,6 @@ public class IrisCaveCarver3D { return true; } - Scratch scratch = scratchCache.get(); int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); @@ -1833,13 +1883,13 @@ public class IrisCaveCarver3D { } private void prepareAdaptivePlaneSampleBounds( + CaveCarveScratch scratch, int[] planeColumnIndices, int planeCount, int adaptiveSampleStep, int[] adaptivePlaneSampleBounds, int axisCells ) { - Scratch scratch = scratchCache.get(); prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisCells + 1); int[] adaptiveCellX = scratch.adaptiveCellX; int[] adaptiveCellZ = scratch.adaptiveCellZ; @@ -1873,6 +1923,7 @@ public class IrisCaveCarver3D { } private void prepareAdaptivePlaneColumns( + CaveCarveScratch scratch, int[] planeColumnIndices, int planeCount, int adaptiveSampleStep, @@ -1883,7 +1934,6 @@ public class IrisCaveCarver3D { double[] adaptivePlanePrediction, double[] adaptivePlaneAmbiguity ) { - Scratch scratch = scratchCache.get(); prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisSamples); int[] adaptiveCellZ = scratch.adaptiveCellZ; int[] adaptiveRow0 = scratch.adaptiveRow0; @@ -1910,7 +1960,7 @@ public class IrisCaveCarver3D { } } - private void prepareAdaptiveGeometry(Scratch scratch, int adaptiveSampleStep, int axisCells, int axisSamples) { + private void prepareAdaptiveGeometry(CaveCarveScratch scratch, int adaptiveSampleStep, int axisCells, int axisSamples) { if (scratch.adaptiveGeometryStep == adaptiveSampleStep && scratch.adaptiveGeometryAxisCells == axisCells) { return; } @@ -1942,18 +1992,17 @@ public class IrisCaveCarver3D { return density * inverseNormalization; } - private double sampleDensityNoWarpModules(int x, int y, int z) { - Scratch scratch = scratchCache.get(); + private double sampleDensityNoWarpModules(CaveCarveScratch scratch, int x, int y, int z) { int activeModuleCount = prepareActiveModules(scratch, y); if (activeModuleCount == 0) { return sampleDensityNoWarpNoModules(x, y, z); } - ModuleState[] localModules = scratch.activeModules; + CaveFieldModuleState[] localModules = scratch.activeModules; double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { - ModuleState module = localModules[moduleIndex]; + CaveFieldModuleState module = localModules[moduleIndex]; double moduleDensity = module.density.noiseFastSigned3D(x, y, z) - module.threshold; if (module.invert) { moduleDensity = -moduleDensity; @@ -1965,8 +2014,7 @@ public class IrisCaveCarver3D { return density * inverseNormalization; } - private double sampleDensityWarpOnly(int x, int y, int z) { - Scratch scratch = scratchCache.get(); + private double sampleDensityWarpOnly(CaveCarveScratch scratch, int x, int y, int z) { int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); @@ -1981,18 +2029,17 @@ public class IrisCaveCarver3D { return density * inverseNormalization; } - private double sampleDensityWarpModules(int x, int y, int z) { - Scratch scratch = scratchCache.get(); + private double sampleDensityWarpModules(CaveCarveScratch scratch, int x, int y, int z) { int activeModuleCount = prepareActiveModules(scratch, y); if (activeModuleCount == 0) { - return sampleDensityWarpOnly(x, y, z); + return sampleDensityWarpOnly(scratch, x, y, z); } - ModuleState[] localModules = scratch.activeModules; + CaveFieldModuleState[] localModules = scratch.activeModules; return sampleDensityWarpModules(scratch, x, y, z, localModules, activeModuleCount); } - private double sampleDensityWarpModules(Scratch scratch, int x, int y, int z, ModuleState[] localModules, int activeModuleCount) { + private double sampleDensityWarpModules(CaveCarveScratch scratch, int x, int y, int z, CaveFieldModuleState[] localModules, int activeModuleCount) { int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); @@ -2005,7 +2052,7 @@ public class IrisCaveCarver3D { double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight; density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight; for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) { - ModuleState module = localModules[moduleIndex]; + CaveFieldModuleState module = localModules[moduleIndex]; double moduleDensity = module.density.noiseFastSigned3D(warpedX, warpedY, warpedZ) - module.threshold; if (module.invert) { moduleDensity = -moduleDensity; @@ -2017,7 +2064,7 @@ public class IrisCaveCarver3D { return density * inverseNormalization; } - private int prepareWarpSample(Scratch scratch, int sx, int sy, int sz) { + private int prepareWarpSample(CaveCarveScratch scratch, int sx, int sy, int sz) { int slot = mixWarpKey(sx, sy, sz) & (scratch.warpCacheX.length - 1); if (scratch.warpCacheSet[slot] && scratch.warpCacheX[slot] == sx @@ -2043,20 +2090,26 @@ public class IrisCaveCarver3D { return hash; } - private int prepareActiveModules(Scratch scratch, int y) { - ModuleState[] configuredModules = modules; + private int prepareActiveModules(CaveCarveScratch scratch, int y) { + CaveFieldModuleState[] configuredModules = modules; int configuredCount = configuredModules.length; if (configuredCount == 0) { return 0; } + // Pure function of (y, modules). modules is final and the scratch is per carver per thread, + // so the band selection and the remaining-contribution prefix sums only change when y does. + if (scratch.activeModulesY == y) { + return scratch.activeModuleCount; + } + if (scratch.activeModules.length < configuredCount) { - scratch.activeModules = new ModuleState[configuredCount]; + scratch.activeModules = new CaveFieldModuleState[configuredCount]; } int activeCount = 0; for (int moduleIndex = 0; moduleIndex < configuredCount; moduleIndex++) { - ModuleState module = configuredModules[moduleIndex]; + CaveFieldModuleState module = configuredModules[moduleIndex]; if (y < module.minY || y > module.maxY) { continue; } @@ -2073,15 +2126,17 @@ public class IrisCaveCarver3D { scratch.activeModuleRemainingMin[activeCount] = 0D; scratch.activeModuleRemainingMax[activeCount] = 0D; for (int moduleIndex = activeCount - 1; moduleIndex >= 0; moduleIndex--) { - ModuleState module = scratch.activeModules[moduleIndex]; + CaveFieldModuleState module = scratch.activeModules[moduleIndex]; scratch.activeModuleRemainingMin[moduleIndex] = scratch.activeModuleRemainingMin[moduleIndex + 1] + module.minContribution; scratch.activeModuleRemainingMax[moduleIndex] = scratch.activeModuleRemainingMax[moduleIndex + 1] + module.maxContribution; } + scratch.activeModulesY = y; + scratch.activeModuleCount = activeCount; return activeCount; } - private MatterSlice resolveCavernSlice(Scratch scratch, MantleChunk chunk, int sectionIndex) { + private MatterSlice resolveCavernSlice(CaveCarveScratch scratch, MantleChunk chunk, int sectionIndex) { @SuppressWarnings("unchecked") MatterSlice cachedSlice = (MatterSlice) scratch.sectionSlices[sectionIndex]; if (cachedSlice != null) { @@ -2099,7 +2154,7 @@ public class IrisCaveCarver3D { return resolvedSlice; } - private MatterCavern[] prepareMatterByYTable(Scratch scratch, int minY, int maxY) { + private MatterCavern[] prepareMatterByYTable(CaveCarveScratch scratch, int minY, int maxY) { int size = Math.max(0, maxY - minY + 1); if (scratch.matterByY.length < size) { scratch.matterByY = new MatterCavern[size]; @@ -2126,15 +2181,6 @@ public class IrisCaveCarver3D { return matterByY; } - private int resolveWaterMaxY(int columnSurfaceY) { - if (!profile.isAllowWater()) { - return Integer.MIN_VALUE; - } - - int minDepth = Math.max(0, profile.getWaterMinDepthBelowSurface()); - return Math.min(engine.getDimension().getFluidHeight(), columnSurfaceY - minDepth); - } - private MatterCavern resolveMatter(MatterCavern verticalMatter, int x, int y, int z, int columnIndex, int[] waterMaxY, double localThreshold) { if (verticalMatter != carveLava @@ -2146,19 +2192,18 @@ public class IrisCaveCarver3D { } private boolean isAquiferCandidate(int x, int y, int z, double localThreshold) { - int fluidHeight = engine.getDimension().getFluidHeight(); double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D)); double cutoff = 0.35D + (depthFactor * 0.2D); if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) { return false; } - return !profile.isWaterRequiresFloor() || hasAquiferCupSupport(x, y, z, localThreshold); + return !waterRequiresFloor || hasAquiferCupSupport(x, y, z, localThreshold); } private boolean hasAquiferCupSupport(int x, int y, int z, double threshold) { int floorY = Math.max(0, y - 1); int deepFloorY = Math.max(0, y - 2); - int aboveY = Math.min(engine.getHeight() - 1, y + 1); + int aboveY = Math.min(aquiferCeilingY, y + 1); if (!isDensitySolid(x, floorY, z, threshold)) { return false; } @@ -2190,14 +2235,14 @@ public class IrisCaveCarver3D { } private void writeCavern(MatterSlice cavernSlice, int localX, int y, int localZ, - MatterCavern matter, WaterSupportPlan waterSupportPlan) { + MatterCavern matter, CaveWaterSupportPlan waterSupportPlan) { cavernSlice.set(localX, y & 15, localZ, matter); if (waterSupportPlan != null && matter == carveWater) { waterSupportPlan.add(localX, y, localZ, carveWater, carveAir); } } - private void prepareSectionCaches(Scratch scratch, int minY, int maxY) { + private void prepareSectionCaches(CaveCarveScratch scratch, int minY, int maxY) { int minSection = Math.max(0, PowerOfTwoCoordinates.floorDivPow2(minY, 4)); int maxSection = Math.max(minSection, PowerOfTwoCoordinates.floorDivPow2(maxY, 4)); int requiredSections = maxSection + 1; @@ -2233,7 +2278,7 @@ public class IrisCaveCarver3D { return (value * 2D) - 1D; } - private double[] prepareVerticalEdgeFadeTable(Scratch scratch, int minY, int maxY) { + private double[] prepareVerticalEdgeFadeTable(CaveCarveScratch scratch, int minY, int maxY) { int size = Math.max(0, maxY - minY + 1); if (scratch.verticalEdgeFade.length < size) { scratch.verticalEdgeFade = new double[size]; @@ -2264,176 +2309,4 @@ public class IrisCaveCarver3D { return verticalEdgeFade; } - - static final class WaterSupportPlan { - private final IdentityHashMap groups = new IdentityHashMap<>(); - - void add(int localX, int y, int localZ, MatterCavern water, MatterCavern air) { - WaterCandidateGroup group = groups.computeIfAbsent(water, key -> new WaterCandidateGroup(water, air)); - int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); - group.positions.set((y << 8) | columnIndex); - } - - void resolve(MantleChunk chunk) { - if (chunk == null) { - groups.clear(); - return; - } - - for (Map.Entry entry : groups.entrySet()) { - WaterCandidateGroup group = entry.getValue(); - for (int position = group.positions.nextSetBit(0); position >= 0; position = group.positions.nextSetBit(position + 1)) { - int y = position >>> 8; - int columnIndex = position & 255; - int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); - int localZ = columnIndex & 15; - MatterCavern current = getCavern(chunk, localX, y, localZ); - if (current != group.water || hasCupSupport(chunk, localX, y, localZ)) { - continue; - } - - Matter section = chunk.get(y >> 4); - MatterSlice cavernSlice = section.getSlice(MatterCavern.class); - cavernSlice.set(localX, y & 15, localZ, group.air); - } - } - groups.clear(); - } - - private static boolean hasCupSupport(MantleChunk chunk, int localX, int y, int localZ) { - if (localX <= 0 || localX >= 15 || localZ <= 0 || localZ >= 15 - || y <= 1 || !isSolid(chunk, localX, y - 1, localZ) - || !isSolid(chunk, localX, y - 2, localZ)) { - return false; - } - - int support = 0; - if (isSolid(chunk, localX + 1, y, localZ)) { - support++; - } - if (isSolid(chunk, localX - 1, y, localZ)) { - support++; - } - if (isSolid(chunk, localX, y, localZ + 1)) { - support++; - } - if (isSolid(chunk, localX, y, localZ - 1)) { - support++; - } - if (isSolid(chunk, localX, y + 1, localZ)) { - support++; - } - return support >= 4; - } - - private static boolean isSolid(MantleChunk chunk, int localX, int y, int localZ) { - if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16) { - return false; - } - MatterCavern cavern = getCavern(chunk, localX, y, localZ); - return cavern == null || !cavern.isCavern(); - } - - private static MatterCavern getCavern(MantleChunk chunk, int localX, int y, int localZ) { - Matter section = chunk.get(y >> 4); - if (section == null) { - return null; - } - - MatterSlice cavernSlice = section.getSlice(MatterCavern.class); - return cavernSlice == null ? null : cavernSlice.get(localX, y & 15, localZ); - } - } - - private static final class WaterCandidateGroup { - private final MatterCavern water; - private final MatterCavern air; - private final BitSet positions = new BitSet(); - - private WaterCandidateGroup(MatterCavern water, MatterCavern air) { - this.water = water; - this.air = air; - } - } - - private static final class ModuleState { - private final CNG density; - private final int minY; - private final int maxY; - private final double weight; - private final double threshold; - private final boolean invert; - private final double minContribution; - private final double maxContribution; - - private ModuleState(IrisCaveFieldModule module, CNG density) { - IrisRange range = module.getVerticalRange(); - this.density = density; - this.minY = (int) Math.floor(range.getMin()); - this.maxY = (int) Math.ceil(range.getMax()); - this.weight = module.getWeight(); - this.threshold = module.getThreshold(); - this.invert = module.isInvert(); - double rawMin = invert ? threshold - 1D : -1D - threshold; - double rawMax = invert ? threshold + 1D : 1D - threshold; - this.minContribution = rawMin * weight; - this.maxContribution = rawMax * weight; - } - - private double sample(double x, int y, double z) { - double sampled = density.noiseFastSigned3D(x, y, z); - return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; - } - - private double sample(double x, double y, double z) { - double sampled = density.noiseFastSigned3D(x, y, z); - return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight; - } - } - - private static final class Scratch { - private final int[] columnMaxY = new int[256]; - private final int[] waterMaxY = new int[256]; - private final int[] surfaceBreakFloorY = new int[256]; - private final boolean[] surfaceBreakColumn = new boolean[256]; - private final double[] columnThreshold = new double[256]; - private final double[] passThreshold = new double[256]; - private final double[] fullWeights = new double[256]; - private final double[] clampedColumnWeights = new double[256]; - private final int[] activeColumnIndices = new int[256]; - private final int[] activeColumnTopY = new int[256]; - private final int[] planeColumnIndices = new int[256]; - private final double[] planeThresholdLimit = new double[256]; - private final boolean[] planeCarve = new boolean[256]; - private final double[] adaptivePlaneDensity = new double[81]; - private final double[] adaptivePlanePrediction = new double[256]; - private final double[] adaptivePlaneAmbiguity = new double[256]; - private final int[] adaptivePlaneSampleBounds = new int[4]; - private final int[] adaptiveCellX = new int[256]; - private final int[] adaptiveCellZ = new int[256]; - private final int[] adaptiveRow0 = new int[256]; - private final int[] adaptiveRow1 = new int[256]; - private final double[] adaptiveTx = new double[256]; - private final double[] adaptiveTz = new double[256]; - private final boolean[] warpCacheSet = new boolean[256]; - private final int[] warpCacheX = new int[256]; - private final int[] warpCacheY = new int[256]; - private final int[] warpCacheZ = new int[256]; - private final double[] warpCacheA = new double[256]; - private final double[] warpCacheB = new double[256]; - private final int[] tileIndices = new int[4]; - private final int[] tileLocalX = new int[4]; - private final int[] tileLocalZ = new int[4]; - private final int[] tileTopY = new int[4]; - private ModuleState[] activeModules = new ModuleState[0]; - private double[] activeModuleRemainingMin = new double[0]; - private double[] activeModuleRemainingMax = new double[0]; - private double[] verticalEdgeFade = new double[0]; - private MatterCavern[] matterByY = new MatterCavern[0]; - private Matter[] sectionMatter = new Matter[0]; - private MatterSlice[] sectionSlices = new MatterSlice[0]; - private int adaptiveGeometryStep = -1; - private int adaptiveGeometryAxisCells = -1; - private boolean fullWeightsInitialized; - } } 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 6c3f459dc..f03cde4b9 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 @@ -62,7 +62,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE]; private static final ThreadLocal BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); - private final Map profileCarvers = new IdentityHashMap<>(); + private final Object profileCarverLock = new Object(); + private volatile Map profileCarvers = new IdentityHashMap<>(); static { int kernelIndex = 0; @@ -90,7 +91,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); List weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); - IrisCaveCarver3D.WaterSupportPlan waterSupportPlan = new IrisCaveCarver3D.WaterSupportPlan(); + CaveWaterSupportPlan waterSupportPlan = new CaveWaterSupportPlan(); for (WeightedProfile weightedProfile : weightedProfiles) { carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, waterSupportPlan); } @@ -104,7 +105,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { @ChunkCoordinates private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, - int[] chunkSurfaceHeights, IrisCaveCarver3D.WaterSupportPlan waterSupportPlan) { + int[] chunkSurfaceHeights, CaveWaterSupportPlan waterSupportPlan) { IrisCaveCarver3D carver = getCarver(weightedProfile.profile); carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights, null, waterSupportPlan); @@ -112,7 +113,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private void carveUpperTerrain(UpperDimensionContext upperCtx, List normalProfiles, MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights, - IrisCaveCarver3D.WaterSupportPlan waterSupportPlan) { + CaveWaterSupportPlan waterSupportPlan) { int chunkHeight = getEngineMantle().getEngine().getHeight(); int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight(); int gap = getDimension().getUpperDimensionGap(); @@ -453,14 +454,21 @@ public class MantleCarvingComponent extends IrisMantleComponent { } private IrisCaveCarver3D getCarver(IrisCaveProfile profile) { - synchronized (profileCarvers) { - IrisCaveCarver3D carver = profileCarvers.get(profile); - if (carver != null) { - return carver; + IrisCaveCarver3D carver = profileCarvers.get(profile); + if (carver != null) { + return carver; + } + + IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile); + synchronized (profileCarverLock) { + IrisCaveCarver3D published = profileCarvers.get(profile); + if (published != null) { + return published; } - IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile); - profileCarvers.put(profile, createdCarver); + Map updated = new IdentityHashMap<>(profileCarvers); + updated.put(profile, createdCarver); + profileCarvers = updated; return createdCarver; } } 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 d4b1b551e..7c9f0e7b0 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 @@ -28,7 +28,6 @@ import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.TreeBlockMaterial; import art.arcane.iris.engine.object.CarvingMode; @@ -49,7 +48,6 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement; import art.arcane.iris.engine.object.IrisProceduralTree; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.ObjectPlaceMode; -import art.arcane.iris.engine.object.TileData; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -81,45 +79,6 @@ public class MantleObjectComponent extends IrisMantleComponent { private static final int BEDROCK_CLEARANCE = 6; private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); - private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec()); - private static final boolean GOLDEN_DEBUG = GOLDEN_DEBUG_TARGET != null; - - private static String resolveGoldenDebugSpec() { - String property = System.getProperty("iris.goldendebug"); - if (property != null && !property.isBlank()) { - return property; - } - try { - java.io.File marker = new java.io.File("plugins/Iris/goldendebug.txt"); - if (marker.isFile()) { - return java.nio.file.Files.readString(marker.toPath()).trim(); - } - } catch (Throwable ignored) { - } - return null; - } - - private static int[] parseGoldenDebugTarget(String raw) { - if (raw == null || raw.isBlank()) { - return null; - } - String[] parts = raw.split(","); - if (parts.length != 2 && parts.length != 3) { - return null; - } - try { - int radius = parts.length == 3 ? Integer.parseInt(parts[2].trim()) : 0; - return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()), radius}; - } catch (NumberFormatException e) { - return null; - } - } - - private static boolean isGoldenDebugChunk(int x, int z) { - return GOLDEN_DEBUG - && Math.abs(GOLDEN_DEBUG_TARGET[0] - x) <= GOLDEN_DEBUG_TARGET[2] - && Math.abs(GOLDEN_DEBUG_TARGET[1] - z) <= GOLDEN_DEBUG_TARGET[2]; - } public MantleObjectComponent(EngineMantle engineMantle) { super(engineMantle, ReservedFlag.OBJECT, 1); @@ -449,7 +408,7 @@ public class MantleObjectComponent extends IrisMantleComponent { int blockX = x << 4; int blockZ = z << 4; - boolean golden = isGoldenDebugChunk(x, z); + boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(x, z); CaveAnchorCache caveAnchorCache = new CaveAnchorCache(); for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) { boolean treePlacement = p instanceof IrisProceduralTree; @@ -474,7 +433,7 @@ public class MantleObjectComponent extends IrisMantleComponent { int minDepthBelowSurface = resolveObjectMinDepthBelowSurface(caveProfile); int anchorSearchAttempts = resolveAnchorSearchAttempts(caveProfile); IObjectPlacer basePlacer = p.isPlausible() ? new DecayControlPlacer(writer) : writer; - IObjectPlacer placer = golden ? new GoldenDebugPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer; + IObjectPlacer placer = golden ? new GoldenDebugObjectPlacer(basePlacer, scope + "/" + p.getName()) : basePlacer; int density = Math.max(1, p.getDensity()); for (int i = 0; i < density; i++) { IrisObject variant = p.getVariantObject(getData(), rng); @@ -554,8 +513,8 @@ public class MantleObjectComponent extends IrisMantleComponent { placeResult = contained.resultY(); commitResult = contained.commitResult(); } else { + String marker = placementMarker(variant, id, "procedural"); placeResult = variant.place(xx, -1, zz, placer, placement, rng, (b, data) -> { - String marker = placementMarker(variant, id, "procedural"); if (marker != null) { placer.setData(b.getX(), b.getY(), b.getZ(), marker); } @@ -671,7 +630,7 @@ public class MantleObjectComponent extends IrisMantleComponent { int nullObjects = 0; int errors = 0; int density = objectPlacement.getDensity(rng, x, z, getData()); - boolean golden = isGoldenDebugChunk(chunkX, chunkZ); + boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(chunkX, chunkZ); for (int i = 0; i < density; i++) { attempts++; @@ -692,7 +651,7 @@ public class MantleObjectComponent extends IrisMantleComponent { IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v); boolean treePlacement = isTreePlacement(v, effectivePlacement); int id = rng.i(0, Integer.MAX_VALUE); - IObjectPlacer placePlacer = golden ? new GoldenDebugPlacer(writer, scope + "/" + v.getLoadKey()) : writer; + IObjectPlacer placePlacer = golden ? new GoldenDebugObjectPlacer(writer, scope + "/" + v.getLoadKey()) : writer; if (golden) { IrisLogging.info("Goldendebug object attempt: chunk=" + chunkX + "," + chunkZ + " scope=" + scope @@ -703,8 +662,8 @@ public class MantleObjectComponent extends IrisMantleComponent { + " mode=" + effectivePlacement.getMode()); } try { + String marker = placementMarker(v, id, "surface"); int result = v.place(xx, -1, zz, placePlacer, effectivePlacement, rng, (b, data) -> { - String marker = placementMarker(v, id, "surface"); if (marker != null) { writer.setData(b.getX(), b.getY(), b.getZ(), marker); } @@ -1073,9 +1032,9 @@ public class MantleObjectComponent extends IrisMantleComponent { placement.setForcePlace(true); } boolean treePlacement = isTreePlacement(v, objectPlacement); + String marker = placementMarker(v, id, "upper"); int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> { - String marker = placementMarker(v, id, "upper"); if (marker != null) { writer.setData(b.getX(), b.getY(), b.getZ(), marker); } @@ -1247,99 +1206,6 @@ public class MantleObjectComponent extends IrisMantleComponent { return maxAnchorY + 1; } - private static final class GoldenDebugPlacer implements IObjectPlacer { - private final IObjectPlacer delegate; - private final String tag; - - private GoldenDebugPlacer(IObjectPlacer delegate, String tag) { - this.delegate = delegate; - this.tag = tag; - } - - @Override - public int getHighest(int x, int z, IrisData data) { - int result = delegate.getHighest(x, z, data); - IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ")=" + result); - return result; - } - - @Override - public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - int result = delegate.getHighest(x, z, data, ignoreFluid); - IrisLogging.info("Goldendebug query: tag=" + tag + " getHighest(" + x + "," + z + ",ignoreFluid=" + ignoreFluid + ")=" + result); - return result; - } - - @Override - public void set(int x, int y, int z, PlatformBlockState d) { - delegate.set(x, y, z, d); - } - - @Override - public PlatformBlockState get(int x, int y, int z) { - return delegate.get(x, y, z); - } - - @Override - public boolean isPreventingDecay() { - return delegate.isPreventingDecay(); - } - - @Override - public boolean isCarved(int x, int y, int z) { - boolean result = delegate.isCarved(x, y, z); - IrisLogging.info("Goldendebug query: tag=" + tag + " isCarved(" + x + "," + y + "," + z + ")=" + result); - return result; - } - - @Override - public boolean isSurfaceSolid(int x, int y, int z) { - return delegate.isSurfaceSolid(x, y, z); - } - - @Override - public boolean isSolid(int x, int y, int z) { - boolean result = delegate.isSolid(x, y, z); - IrisLogging.info("Goldendebug query: tag=" + tag + " isSolid(" + x + "," + y + "," + z + ")=" + result); - return result; - } - - @Override - public boolean isUnderwater(int x, int z) { - return delegate.isUnderwater(x, z); - } - - @Override - public int getFluidHeight() { - return delegate.getFluidHeight(); - } - - @Override - public boolean isDebugSmartBore() { - return delegate.isDebugSmartBore(); - } - - @Override - public void setData(int xx, int yy, int zz, T data) { - delegate.setData(xx, yy, zz, data); - } - - @Override - public T getData(int xx, int yy, int zz, Class t) { - return delegate.getData(xx, yy, zz, t); - } - - @Override - public void setTile(int xx, int yy, int zz, TileData tile) { - delegate.setTile(xx, yy, zz, tile); - } - - @Override - public Engine getEngine() { - return delegate.getEngine(); - } - } - private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, CaveAnchorCache anchorCache) { KList anchors = anchorCache.get(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z); if (anchors.isEmpty()) { 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 10ea2fb8b..3741bab65 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 @@ -104,12 +104,14 @@ public class IrisCarveModifier extends EngineAssignedModifier mc = mantle.getChunk(x, z).use(); try { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); + final int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); + final int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> { if (c == null) { return; } - if (yy >= getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() || yy <= 0) { + if (yy >= worldHeightSpan || yy <= 0) { return; } @@ -138,10 +140,12 @@ public class IrisCarveModifier extends EngineAssignedModifier output, boolean multicore, ChunkContext context) { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java index d45e9ffdb..11be36b57 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java @@ -20,7 +20,9 @@ package art.arcane.iris.engine.modifier; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisProceduralBlocks; import art.arcane.iris.engine.object.IrisSlopeClip; import art.arcane.iris.util.project.context.ChunkContext; @@ -49,21 +51,54 @@ public class IrisPostModifier extends EngineAssignedModifier Hunk sync = output.synchronize(); int width = output.getWidth(); int depth = output.getDepth(); + int planeWidth = width + 2; + int[] heights = heightPlane(x, z, width, depth, planeWidth); + IrisDimension dimension = getDimension(); + boolean walls = dimension.isPostProcessingWalls(); + boolean slabs = dimension.isPostProcessingSlabs(); + int fluidHeight = dimension.getFluidHeight(); for (int i = 0; i < width; i++) { for (int j = 0; j < depth; j++) { - post(i, j, sync, i + x, j + z, context); + post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight); } } getEngine().getMetrics().getPost().put(p.getMilliseconds()); } - private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context) { - int h = getEngine().getMantle().trueHeight(x, z); - int ha = getEngine().getMantle().trueHeight(x + 1, z); - int hb = getEngine().getMantle().trueHeight(x, z + 1); - int hc = getEngine().getMantle().trueHeight(x - 1, z); - int hd = getEngine().getMantle().trueHeight(x, z - 1); + /** + * Every column reads its own height plus its four neighbours, so adjacent columns would otherwise + * resolve the same height stream entry up to five times. Resolve the padded plane once instead. The + * four diagonal corners are never read, so they are left unresolved. + */ + private int[] heightPlane(int x, int z, int width, int depth, int planeWidth) { + int[] heights = new int[planeWidth * (depth + 2)]; + EngineMantle mantle = getEngine().getMantle(); + + for (int j = -1; j <= depth; j++) { + boolean edge = j == -1 || j == depth; + int from = edge ? 0 : -1; + int to = edge ? width - 1 : width; + int row = (j + 1) * planeWidth; + + for (int i = from; i <= to; i++) { + heights[row + i + 1] = mantle.trueHeight(x + i, z + j); + } + } + + return heights; + } + + private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) { + // x/z are world coordinates, the hunk is indexed relative to this chunk origin. + int originX = x - currentPostX; + int originZ = z - currentPostZ; + int center = (currentPostZ + 1) * planeWidth + currentPostX + 1; + int h = heights[center]; + int ha = heights[center + 1]; + int hb = heights[center + planeWidth]; + int hc = heights[center - 1]; + int hd = heights[center - planeWidth]; // Floating Nibs int g = 0; @@ -77,11 +112,11 @@ public class IrisPostModifier extends EngineAssignedModifier g += hc < h - 1 ? 1 : 0; g += hd < h - 1 ? 1 : 0; - if (g == 4 && isAir(x, h - 1, z, currentPostX, currentPostZ, currentData)) { - setPostBlock(x, h, z, States.AIR, currentPostX, currentPostZ, currentData); + if (g == 4 && isAir(x, h - 1, z, originX, originZ, currentData)) { + setPostBlock(x, h, z, States.AIR, originX, originZ, currentData); for (int i = h - 1; i > 0; i--) { - if (!isAir(x, i, z, currentPostX, currentPostZ, currentData)) { + if (!isAir(x, i, z, originX, originZ, currentData)) { h = i; break; } @@ -96,12 +131,12 @@ public class IrisPostModifier extends EngineAssignedModifier g += hd == h - 1 ? 1 : 0; if (g >= 4) { - PlatformBlockState bcState = getPostBlock(x, h, z, currentPostX, currentPostZ, currentData); - PlatformBlockState bState = getPostBlock(x, h + 1, z, currentPostX, currentPostZ, currentData); + PlatformBlockState bcState = getPostBlock(x, h, z, originX, originZ, currentData); + PlatformBlockState bState = getPostBlock(x, h + 1, z, originX, originZ, currentData); if (bState.isOccluding() && bState.isSolid()) { if (bcState.isSolid()) { - setPostBlock(x, h, z, bState, currentPostX, currentPostZ, currentData); + setPostBlock(x, h, z, bState, originX, originZ, currentData); h--; } } @@ -114,10 +149,10 @@ public class IrisPostModifier extends EngineAssignedModifier g += hd == h + 1 ? 1 : 0; if (g >= 4) { - PlatformBlockState ba = getPostBlock(x, ha, z, currentPostX, currentPostZ, currentData); - PlatformBlockState bb = getPostBlock(x, hb, z, currentPostX, currentPostZ, currentData); - PlatformBlockState bc = getPostBlock(x, hc, z, currentPostX, currentPostZ, currentData); - PlatformBlockState bd = getPostBlock(x, hd, z, currentPostX, currentPostZ, currentData); + PlatformBlockState ba = getPostBlock(x, ha, z, originX, originZ, currentData); + PlatformBlockState bb = getPostBlock(x, hb, z, originX, originZ, currentData); + PlatformBlockState bc = getPostBlock(x, hc, z, originX, originZ, currentData); + PlatformBlockState bd = getPostBlock(x, hd, z, originX, originZ, currentData); g = 0; g = B.isSolid(ba) ? g + 1 : g; g = B.isSolid(bb) ? g + 1 : g; @@ -125,7 +160,7 @@ public class IrisPostModifier extends EngineAssignedModifier g = B.isSolid(bd) ? g + 1 : g; if (g >= 3) { - setPostBlock(x, h + 1, z, getPostBlock(x, h, z, currentPostX, currentPostZ, currentData), currentPostX, currentPostZ, currentData); + setPostBlock(x, h + 1, z, getPostBlock(x, h, z, originX, originZ, currentData), originX, originZ, currentData); h++; } } @@ -134,7 +169,7 @@ public class IrisPostModifier extends EngineAssignedModifier // Wall Patcher IrisBiome biome = context.getBiome().get(currentPostX, currentPostZ); - if (getDimension().isPostProcessingWalls()) { + if (walls) { if (!biome.getWall().getPalette().isEmpty()) { if (ha < h - 2 || hb < h - 2 || hc < h - 2 || hd < h - 2) { boolean brokeGround = false; @@ -144,7 +179,7 @@ public class IrisPostModifier extends EngineAssignedModifier PlatformBlockState d = biome.getWall().get(rng, x + i, i + h, z + i, getData()); if (d != null) { - if (isAirOrWater(x, i, z, currentPostX, currentPostZ, currentData)) { + if (isAirOrWater(x, i, z, originX, originZ, currentData)) { if (brokeGround) { break; } @@ -152,7 +187,7 @@ public class IrisPostModifier extends EngineAssignedModifier continue; } - setPostBlock(x, i, z, d, currentPostX, currentPostZ, currentData); + setPostBlock(x, i, z, d, originX, originZ, currentData); brokeGround = true; } } @@ -161,12 +196,12 @@ public class IrisPostModifier extends EngineAssignedModifier } // Slab - if (getDimension().isPostProcessingSlabs()) { + if (slabs) { //@builder - if ((ha == h + 1 && isSolidNonSlab(x + 1, ha, z, currentPostX, currentPostZ, currentData)) - || (hb == h + 1 && isSolidNonSlab(x, hb, z + 1, currentPostX, currentPostZ, currentData)) - || (hc == h + 1 && isSolidNonSlab(x - 1, hc, z, currentPostX, currentPostZ, currentData)) - || (hd == h + 1 && isSolidNonSlab(x, hd, z - 1, currentPostX, currentPostZ, currentData))) + if ((ha == h + 1 && isSolidNonSlab(x + 1, ha, z, originX, originZ, currentData)) + || (hb == h + 1 && isSolidNonSlab(x, hb, z + 1, originX, originZ, currentData)) + || (hc == h + 1 && isSolidNonSlab(x - 1, hc, z, originX, originZ, currentData)) + || (hd == h + 1 && isSolidNonSlab(x, hd, z - 1, originX, originZ, currentData))) //@done { IrisSlopeClip sc = biome.getSlab().getSlopeCondition(); @@ -175,16 +210,16 @@ public class IrisPostModifier extends EngineAssignedModifier if (d != null) { boolean cancel = B.isAir(d); - if (IrisProceduralBlocks.materialKey(d).equals("minecraft:snow") && h + 1 <= getDimension().getFluidHeight()) { + if (IrisProceduralBlocks.materialKey(d).equals("minecraft:snow") && h + 1 <= fluidHeight) { cancel = true; } - if (isSnowLayer(x, h, z, currentPostX, currentPostZ, currentData)) { + if (isSnowLayer(x, h, z, originX, originZ, currentData)) { cancel = true; } - if (!cancel && isAirOrWater(x, h + 1, z, currentPostX, currentPostZ, currentData)) { - setPostBlock(x, h + 1, z, d, currentPostX, currentPostZ, currentData); + if (!cancel && isAirOrWater(x, h + 1, z, originX, originZ, currentData)) { + setPostBlock(x, h + 1, z, d, originX, originZ, currentData); h++; } } @@ -192,30 +227,30 @@ public class IrisPostModifier extends EngineAssignedModifier } // Waterlogging - PlatformBlockState b = getPostBlock(x, h, z, currentPostX, currentPostZ, currentData); + PlatformBlockState b = getPostBlock(x, h, z, originX, originZ, currentData); if (IrisProceduralBlocks.hasProperty(b, "waterlogged")) { boolean w = false; - if (h <= getDimension().getFluidHeight() + 1) { - if (isWaterOrWaterlogged(x, h + 1, z, currentPostX, currentPostZ, currentData)) { + if (h <= fluidHeight + 1) { + if (isWaterOrWaterlogged(x, h + 1, z, originX, originZ, currentData)) { w = true; - } else if ((isWaterOrWaterlogged(x + 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, currentPostX, currentPostZ, currentData))) { + } else if ((isWaterOrWaterlogged(x + 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, originX, originZ, currentData))) { w = true; } } if (w != "true".equals(IrisProceduralBlocks.propertyValue(b, "waterlogged"))) { - setPostBlock(x, h, z, b.withProperty("waterlogged", String.valueOf(w)), currentPostX, currentPostZ, currentData); + setPostBlock(x, h, z, b.withProperty("waterlogged", String.valueOf(w)), originX, originZ, currentData); } - } else if (IrisProceduralBlocks.materialKey(b).equals("minecraft:air") && h <= getDimension().getFluidHeight()) { - if ((isWaterOrWaterlogged(x + 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, currentPostX, currentPostZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, currentPostX, currentPostZ, currentData))) { - setPostBlock(x, h, z, States.WATER, currentPostX, currentPostZ, currentData); + } else if (IrisProceduralBlocks.materialKey(b).equals("minecraft:air") && h <= fluidHeight) { + if ((isWaterOrWaterlogged(x + 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x - 1, h, z, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z + 1, originX, originZ, currentData) || isWaterOrWaterlogged(x, h, z - 1, originX, originZ, currentData))) { + setPostBlock(x, h, z, States.WATER, originX, originZ, currentData); } } // Foliage - b = getPostBlock(x, h + 1, z, currentPostX, currentPostZ, currentData); + b = getPostBlock(x, h + 1, z, originX, originZ, currentData); if (B.isVineBlock(b)) { PlatformBlockState result = b; @@ -226,77 +261,87 @@ public class IrisPostModifier extends EngineAssignedModifier continue; } int[] mod = IrisProceduralBlocks.faceOffset(face); - PlatformBlockState d = getPostBlock(x + mod[0], finalH + mod[1], z + mod[2], currentPostX, currentPostZ, currentData); + PlatformBlockState d = getPostBlock(x + mod[0], finalH + mod[1], z + mod[2], originX, originZ, currentData); result = result.withProperty(face, String.valueOf(!B.isAir(d) && !B.isVineBlock(d))); } if (!result.equals(b)) { - setPostBlock(x, h + 1, z, result, currentPostX, currentPostZ, currentData); + setPostBlock(x, h + 1, z, result, originX, originZ, currentData); } } if (B.isFoliage(b) || IrisProceduralBlocks.materialKey(b).equals("minecraft:dead_bush")) { - PlatformBlockState onto = getPostBlock(x, h, z, currentPostX, currentPostZ, currentData); + PlatformBlockState onto = getPostBlock(x, h, z, originX, originZ, currentData); if (!B.canPlaceOnto(b, onto) && !B.isDecorant(b)) { - setPostBlock(x, h + 1, z, States.AIR, currentPostX, currentPostZ, currentData); + setPostBlock(x, h + 1, z, States.AIR, originX, originZ, currentData); } } } - public boolean isAir(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData)); + public boolean isAir(int x, int y, int z, int originX, int originZ, Hunk currentData) { + String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)); return material.equals("minecraft:air") || material.equals("minecraft:cave_air"); } - public boolean hasGravity(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData)); + public boolean hasGravity(int x, int y, int z, int originX, int originZ, Hunk currentData) { + String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)); return material.equals("minecraft:sand") || material.equals("minecraft:red_sand") || material.endsWith("_concrete_powder"); } - public boolean isSolid(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - PlatformBlockState d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); + public boolean isSolid(int x, int y, int z, int originX, int originZ, Hunk currentData) { + PlatformBlockState d = getPostBlock(x, y, z, originX, originZ, currentData); return B.isSolid(d) && !B.isVineBlock(d); } - public boolean isSolidNonSlab(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - PlatformBlockState d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); + public boolean isSolidNonSlab(int x, int y, int z, int originX, int originZ, Hunk currentData) { + PlatformBlockState d = getPostBlock(x, y, z, originX, originZ, currentData); return B.isSolid(d) && !IrisProceduralBlocks.materialKey(d).endsWith("_slab"); } - public boolean isAirOrWater(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData)); + public boolean isAirOrWater(int x, int y, int z, int originX, int originZ, Hunk currentData) { + String material = IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)); return material.equals("minecraft:water") || material.equals("minecraft:air") || material.equals("minecraft:cave_air"); } - public boolean isSlab(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData)).endsWith("_slab"); + public boolean isSlab(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)).endsWith("_slab"); } - public boolean isSnowLayer(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData)).equals("minecraft:snow"); + public boolean isSnowLayer(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)).equals("minecraft:snow"); } - public boolean isWater(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData)).equals("minecraft:water"); + public boolean isWater(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.materialKey(getPostBlock(x, y, z, originX, originZ, currentData)).equals("minecraft:water"); } - public boolean isWaterOrWaterlogged(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - PlatformBlockState d = getPostBlock(x, y, z, currentPostX, currentPostZ, currentData); + public boolean isWaterOrWaterlogged(int x, int y, int z, int originX, int originZ, Hunk currentData) { + PlatformBlockState d = getPostBlock(x, y, z, originX, originZ, currentData); return IrisProceduralBlocks.materialKey(d).equals("minecraft:water") || "true".equals(IrisProceduralBlocks.propertyValue(d, "waterlogged")); } - public boolean isLiquid(int x, int y, int z, int currentPostX, int currentPostZ, Hunk currentData) { - return IrisProceduralBlocks.hasProperty(getPostBlock(x, y, z, currentPostX, currentPostZ, currentData), "level"); + public boolean isLiquid(int x, int y, int z, int originX, int originZ, Hunk currentData) { + return IrisProceduralBlocks.hasProperty(getPostBlock(x, y, z, originX, originZ, currentData), "level"); } - public void setPostBlock(int x, int y, int z, PlatformBlockState d, int currentPostX, int currentPostZ, Hunk currentData) { - if (y < currentData.getHeight()) { - currentData.set(x & 15, y, z & 15, d); + public void setPostBlock(int x, int y, int z, PlatformBlockState d, int originX, int originZ, Hunk currentData) { + int lx = x - originX; + int lz = z - originZ; + + if (lx < 0 || lz < 0 || lx >= currentData.getWidth() || lz >= currentData.getDepth() || y < 0 || y >= currentData.getHeight()) { + return; } + + currentData.set(lx, y, lz, d); } - public PlatformBlockState getPostBlock(int x, int y, int z, int cpx, int cpz, Hunk h) { - PlatformBlockState b = h.getClosest(x & 15, y, z & 15); + /** + * Neighbour columns can sit one block outside this chunk, and the blocks of an adjacent chunk are not + * available while generating this one. Resolve the hunk index relative to the chunk origin and let + * getClosest clamp to the nearest in-chunk column instead of wrapping to the opposite chunk edge. + */ + public PlatformBlockState getPostBlock(int x, int y, int z, int originX, int originZ, Hunk h) { + PlatformBlockState b = h.getClosest(x - originX, y, z - originZ); return b == null ? States.AIR : b; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IRare.java b/core/src/main/java/art/arcane/iris/engine/object/IRare.java index a2dce98cc..963d80d27 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IRare.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IRare.java @@ -74,13 +74,13 @@ public interface IRare { double total = 0; for (T i : possibilities) { - total += 1d / i.getRarity(); + total += 1d / IRare.get(i); } double threshold = total * noiseValue; double buffer = 0; for (T i : possibilities) { - buffer += 1d / i.getRarity(); + buffer += 1d / IRare.get(i); if (buffer >= threshold) { return i; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index eaac977e3..2db001f8b 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -31,15 +31,10 @@ import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.iris.engine.object.annotations.RegistryListResource; import art.arcane.iris.engine.object.annotations.Required; -import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; -import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.DataProvider; -import art.arcane.iris.util.common.data.registry.RegistryUtil; -import art.arcane.volmlib.util.data.VanillaBiomeColors; -import art.arcane.volmlib.util.inventorygui.RandomColor; import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; @@ -54,13 +49,19 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; import art.arcane.iris.spi.PlatformBlockState; -import org.bukkit.NamespacedKey; import org.bukkit.block.Biome; import java.awt.Color; import java.util.EnumMap; import java.util.Objects; +/** + * Represents a biome in a pack. This type is Gson deserialized straight out of user pack JSON, so + * the field block and the transient {@link AtomicCache} block below are load bearing and must not + * move. Behavior lives in the same-package companions: + * {@link IrisBiomeLayerGenerator}, {@link IrisBiomeDerivatives}, {@link IrisBiomeOres}, + * {@link IrisBiomeColorRenderer} and {@link IrisBiomeGenLinks}. + */ @Accessors(chain = true) @NoArgsConstructor @Desc("Represents a biome in iris. Biomes are placed inside of regions and hold objects.\nA biome consists of layers (block palletes), decorations, objects & generators.") @@ -69,10 +70,6 @@ import java.util.Objects; public class IrisBiome extends IrisRegistrant implements IRare { private static final int BIOME_GENERATOR_CACHE_SIZE = 8; - private static final class States { - private static final PlatformBlockState BARRIER = B.getState("BARRIER"); - } - private final transient AtomicCache> genCache = new AtomicCache<>(); private final transient AtomicCache> genCacheMax = new AtomicCache<>(); private final transient AtomicCache> genCacheMin = new AtomicCache<>(); @@ -226,24 +223,23 @@ public class IrisBiome extends IrisRegistrant implements IRare { private KList ores = new KList<>(); public PlatformBlockState generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { - KList localOres = surface ? getSurfaceOres() : getUndergroundOres(); - return generateOres(localOres, x, y, z, rng, data); + return IrisBiomeOres.generateOres(this, x, y, z, rng, data, surface); } public PlatformBlockState generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) { - return generateOres(getSurfaceOres(), x, y, z, rng, data); + return IrisBiomeOres.generateSurfaceOres(this, x, y, z, rng, data); } public PlatformBlockState generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) { - return generateOres(getUndergroundOres(), x, y, z, rng, data); + return IrisBiomeOres.generateUndergroundOres(this, x, y, z, rng, data); } public boolean hasSurfaceOres() { - return !getSurfaceOres().isEmpty(); + return IrisBiomeOres.hasSurfaceOres(this); } public boolean hasUndergroundOres() { - return !getUndergroundOres().isEmpty(); + return IrisBiomeOres.hasUndergroundOres(this); } public synchronized IrisBiome setInferredType(InferredType inferredType) { @@ -277,22 +273,10 @@ public class IrisBiome extends IrisRegistrant implements IRare { return variant; } - private PlatformBlockState generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { - if (localOres.isEmpty()) { - return null; - } - - int oreCount = localOres.size(); - for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { - IrisOreGenerator oreGenerator = localOres.get(oreIndex); - PlatformBlockState ore = oreGenerator.generate(x, y, z, rng, data); - if (ore != null) { - return ore; - } - } - return null; - } - + /** + * Hand written override of the Lombok setter. It must stay here because it invalidates the ore + * caches that {@link IrisBiomeOres} reads. + */ public void setOres(KList ores) { this.ores = ores == null ? new KList<>() : ores; surfaceOreCache.reset(); @@ -302,118 +286,68 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public KList getSurfaceOreGenerators() { - return getOres(true); + return IrisBiomeOres.getSurfaceOreGenerators(this); } public KList getUndergroundOreGenerators() { - return getOres(false); + return IrisBiomeOres.getUndergroundOreGenerators(this); } public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() { - return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres())); + return IrisBiomeOres.getSurfaceOreGeneratorBounds(this); } public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() { - return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres())); - } - - private KList getSurfaceOres() { - return getOres(true); - } - - private KList getUndergroundOres() { - return getOres(false); - } - - private KList getOres(boolean surface) { - AtomicCache> oreCache = surface ? surfaceOreCache : undergroundOreCache; - return oreCache.aquire(() -> { - KList filtered = new KList<>(); - KList localOres = ores; - int oreCount = localOres.size(); - for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { - IrisOreGenerator oreGenerator = localOres.get(oreIndex); - if (oreGenerator.isGenerateSurface() == surface) { - filtered.add(oreGenerator); - } - } - - return filtered; - }); + return IrisBiomeOres.getUndergroundOreGeneratorBounds(this); } public Biome getDerivative() { - return derivativeResolved.aquire(() -> resolveBiomeKey(derivative)); + Biome cached = derivativeResolved.getIfPresent(); + + if (cached != null) { + return cached; + } + + return derivativeResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKey(derivative)); } public Biome getVanillaDerivative() { Biome resolved = vanillaDerivative == null ? null - : vanillaDerivativeResolved.aquire(() -> resolveBiomeKey(vanillaDerivative)); + : vanillaDerivativeResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKey(vanillaDerivative)); return resolved == null ? getDerivative() : resolved; } public String getVanillaDerivativeKey() { - String resolved = namespacedBiomeKey(vanillaDerivative); - return resolved == null ? namespacedBiomeKey(derivative) : resolved; + return IrisBiomeDerivatives.getVanillaDerivativeKey(derivative, vanillaDerivative); } public String getStructureDerivativeKey() { - String key = getVanillaDerivativeKey(); - if (key == null || !key.startsWith("minecraft:")) { - return key; - } - if (isSea() && !isVanillaSeaStructureBiome(key)) { - return "minecraft:the_void"; - } - if (isShore() && !isVanillaShoreStructureBiome(key)) { - return "minecraft:beach"; - } - return key; + return IrisBiomeDerivatives.getStructureDerivativeKey(this); } public String getDerivativeKey() { - return namespacedBiomeKey(derivative); + return IrisBiomeDerivatives.namespacedBiomeKey(derivative); } - private static String namespacedBiomeKey(String key) { - if (key == null || key.isBlank()) { - return null; + KList getBiomeScatterResolved() { + KList cached = biomeScatterResolved.getIfPresent(); + + if (cached != null) { + return cached; } - String trimmed = key.trim(); - return trimmed.indexOf(':') >= 0 ? trimmed : "minecraft:" + trimmed; + + return biomeScatterResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKeys(biomeScatter)); } - static boolean isVanillaSeaStructureBiome(String key) { - return key != null && (key.contains("ocean") || key.endsWith("river")); - } + KList getBiomeSkyScatterResolved() { + KList cached = biomeSkyScatterResolved.getIfPresent(); - static boolean isVanillaShoreStructureBiome(String key) { - return key != null && (key.endsWith("beach") || key.endsWith("shore")); - } - - private KList getBiomeScatterResolved() { - return biomeScatterResolved.aquire(() -> resolveBiomeKeys(biomeScatter)); - } - - private KList getBiomeSkyScatterResolved() { - return biomeSkyScatterResolved.aquire(() -> resolveBiomeKeys(biomeSkyScatter)); - } - - private static KList resolveBiomeKeys(KList keys) { - KList resolved = new KList<>(); - for (String key : keys) { - resolved.add(resolveBiomeKey(key)); + if (cached != null) { + return cached; } - return resolved; - } - private static Biome resolveBiomeKey(String key) { - if (key == null) { - return null; - } - NamespacedKey namespacedKey = NamespacedKey.fromString(key); - return namespacedKey == null ? null : RegistryUtil.lookup(Biome.class).get(namespacedKey); + return biomeSkyScatterResolved.aquire(() -> IrisBiomeDerivatives.resolveBiomeKeys(biomeSkyScatter)); } public boolean isCustom() { @@ -421,74 +355,15 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public double getGenLinkMax(String loadKey, Engine engine) { - if (loadKey == null || loadKey.isBlank()) { - return 0; - } - - Integer v = genCacheMax.aquire(() -> - { - KMap l = new KMap<>(); - - for (IrisBiomeGeneratorLink i : getGenerators()) { - String generatorKey = i.getGenerator(); - if (generatorKey == null || generatorKey.isBlank()) { - continue; - } - - l.put(generatorKey, i.getMax()); - - } - - return l; - }).get(loadKey); - - return v == null ? 0 : v; + return IrisBiomeGenLinks.getGenLinkMax(this, loadKey, engine); } public double getGenLinkMin(String loadKey, Engine engine) { - if (loadKey == null || loadKey.isBlank()) { - return 0; - } - - Integer v = genCacheMin.aquire(() -> - { - KMap l = new KMap<>(); - - for (IrisBiomeGeneratorLink i : getGenerators()) { - String generatorKey = i.getGenerator(); - if (generatorKey == null || generatorKey.isBlank()) { - continue; - } - - l.put(generatorKey, i.getMin()); - } - - return l; - }).get(loadKey); - - return v == null ? 0 : v; + return IrisBiomeGenLinks.getGenLinkMin(this, loadKey, engine); } public IrisBiomeGeneratorLink getGenLink(String loadKey) { - if (loadKey == null || loadKey.isBlank()) { - return null; - } - - return genCache.aquire(() -> - { - KMap l = new KMap<>(); - - for (IrisBiomeGeneratorLink i : getGenerators()) { - String generatorKey = i.getGenerator(); - if (generatorKey == null || generatorKey.isBlank()) { - continue; - } - - l.put(generatorKey, i); - } - - return l; - }).get(loadKey); + return IrisBiomeGenLinks.getGenLink(this, loadKey); } public IrisBiome getRealCarvingBiome(IrisData data) { @@ -585,250 +460,43 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public KList generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { - if (isLockLayers()) { - return generateLockedLayers(wx, wz, random, maxDepth, height, rdata, complex); - } - - KList data = new KList<>(); - - if (maxDepth <= 0) { - return data; - } - - for (int i = 0; i < layers.size(); i++) { - CNG hgen = getLayerHeightGenerators(random, rdata).get(i); - double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom()); - - IrisSlopeClip sc = getLayers().get(i).getSlopeCondition(); - - if (!sc.isDefault()) { - if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { - d = 0; - } - } - - if (d <= 0) { - continue; - } - - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - - if (data.size() >= maxDepth) { - break; - } - - if (dim.isExplodeBiomePalettes()) { - for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { - data.add(States.BARRIER); - - if (data.size() >= maxDepth) { - break; - } - } - } - } - - return data; + return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex); } public KList generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { - KList data = new KList<>(); - - if (maxDepth <= 0) { - return data; - } - - for (int i = 0; i < caveCeilingLayers.size(); i++) { - CNG hgen = getLayerHeightGenerators(random, rdata).get(i); - double d = hgen.fit(caveCeilingLayers.get(i).getMinHeight(), caveCeilingLayers.get(i).getMaxHeight(), wx / caveCeilingLayers.get(i).getZoom(), wz / caveCeilingLayers.get(i).getZoom()); - - if (d <= 0) { - continue; - } - - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(getCaveCeilingLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / caveCeilingLayers.get(i).getZoom(), j, (wz - j) / caveCeilingLayers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - - if (data.size() >= maxDepth) { - break; - } - - if (dim.isExplodeBiomePalettes()) { - for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { - data.add(States.BARRIER); - - if (data.size() >= maxDepth) { - break; - } - } - } - } - - return data; + return IrisBiomeLayerGenerator.generateCeilingLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex); } public KList generateLockedLayers(double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) { - KList data = new KList<>(); - KList real = new KList<>(); - int maxDepth = Math.min(maxDepthf, getLockLayersMax()); - if (maxDepth <= 0) { - return data; - } - - for (int i = 0; i < layers.size(); i++) { - CNG hgen = getLayerHeightGenerators(random, rdata).get(i); - double d = hgen.fit(layers.get(i).getMinHeight(), layers.get(i).getMaxHeight(), wx / layers.get(i).getZoom(), wz / layers.get(i).getZoom()); - - IrisSlopeClip sc = getLayers().get(i).getSlopeCondition(); - - if (!sc.isDefault()) { - if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { - d = 0; - } - } - - if (d <= 0) { - continue; - } - - for (int j = 0; j < d; j++) { - try { - data.add(getLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / layers.get(i).getZoom(), j, (wz - j) / layers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - } - - if (data.isEmpty()) { - return real; - } - - for (int i = 0; i < maxDepth; i++) { - int offset = (512 - height) - i; - int index = offset % data.size(); - real.add(data.get(Math.max(index, 0))); - } - - return real; - } - - public int getMaxHeight(Engine engine) { - return maxHeight.aquire(() -> - { - int maxHeight = 0; - - for (IrisBiomeGeneratorLink i : getGenerators()) { - maxHeight += i.getMax(); - } - - return maxHeight; - }); - } - - public int getMaxWithObjectHeight(IrisData data, Engine engine) { - return maxWithObjectHeight.aquire(() -> - { - int maxHeight = 0; - - for (IrisBiomeGeneratorLink i : getGenerators()) { - maxHeight += i.getMax(); - } - - int gg = 0; - - for (IrisObjectPlacement i : getObjects()) { - for (IrisObject j : data.getObjectLoader().loadAll(i.getPlace())) { - gg = Math.max(gg, j.getH()); - } - } - - return maxHeight + gg + 3; - }); + return IrisBiomeLayerGenerator.generateLockedLayers(this, wx, wz, random, maxDepthf, height, rdata, complex); } public KList generateSeaLayers(double wx, double wz, RNG random, int maxDepth, IrisData rdata) { - KList data = new KList<>(); + return IrisBiomeLayerGenerator.generateSeaLayers(this, wx, wz, random, maxDepth, rdata); + } - for (int i = 0; i < seaLayers.size(); i++) { - CNG hgen = getLayerSeaHeightGenerators(random, rdata).get(i); - int d = hgen.fit(seaLayers.get(i).getMinHeight(), seaLayers.get(i).getMaxHeight(), wx / seaLayers.get(i).getZoom(), wz / seaLayers.get(i).getZoom()); + /** + * Note the arity split: {@code getMaxHeight()} is the Lombok getter for the height cache field, + * {@code getMaxHeight(Engine)} is the resolved biome height. + */ + public int getMaxHeight(Engine engine) { + return IrisBiomeLayerGenerator.getMaxHeight(this, engine); + } - if (d < 0) { - continue; - } - - for (int j = 0; j < d; j++) { - if (data.size() >= maxDepth) { - break; - } - - try { - data.add(getSeaLayers().get(i).get(random.nextParallelRNG(i + j), (wx + j) / seaLayers.get(i).getZoom(), j, (wz - j) / seaLayers.get(i).getZoom(), rdata)); - } catch (Throwable e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - - if (data.size() >= maxDepth) { - break; - } - } - - return data; + public int getMaxWithObjectHeight(IrisData data, Engine engine) { + return IrisBiomeLayerGenerator.getMaxWithObjectHeight(this, data, engine); } public KList getLayerHeightGenerators(RNG rng, IrisData rdata) { - return layerHeightGenerators.aquire(() -> - { - KList layerHeightGenerators = new KList<>(); - - int m = 7235; - - for (IrisBiomePaletteLayer i : getLayers()) { - layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata)); - } - - return layerHeightGenerators; - }); + return IrisBiomeLayerGenerator.getLayerHeightGenerators(this, rng, rdata); } public KList getLayerSeaHeightGenerators(RNG rng, IrisData data) { - return layerSeaHeightGenerators.aquire(() -> - { - KList layerSeaHeightGenerators = new KList<>(); + return IrisBiomeLayerGenerator.getLayerSeaHeightGenerators(this, rng, data); + } - int m = 7735; - - for (IrisBiomePaletteLayer i : getSeaLayers()) { - layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data)); - } - - return layerSeaHeightGenerators; - }); + public PlatformBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) { + return IrisBiomeLayerGenerator.getSurfaceBlock(this, x, z, rng, idm); } public boolean isLand() { @@ -859,31 +527,19 @@ public class IrisBiome extends IrisRegistrant implements IRare { } public Biome getSkyBiome(RNG rng, double x, double y, double z) { - return getSkyBiome(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + return IrisBiomeDerivatives.getSkyBiome(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); } public Biome getSkyBiome(RNG rng, Engine engine, double x, double y, double z) { - if (biomeSkyScatter.size() == 1) { - return getBiomeSkyScatterResolved().get(0); - } - - if (biomeSkyScatter.isEmpty()) { - return getGroundBiome(rng, engine, x, y, z); - } - - return getBiomeSkyScatterResolved().get(getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z)); + return IrisBiomeDerivatives.getSkyBiome(this, rng, engine, x, y, z); } public IrisBiomeCustom getCustomBiome(RNG rng, double x, double y, double z) { - return getCustomBiome(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + return IrisBiomeDerivatives.getCustomBiome(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); } public IrisBiomeCustom getCustomBiome(RNG rng, Engine engine, double x, double y, double z) { - if (customDerivitives.size() == 1) { - return customDerivitives.get(0); - } - - return customDerivitives.get(getBiomeGenerator(rng, engine).fit(0, customDerivitives.size() - 1, x, y, z)); + return IrisBiomeDerivatives.getCustomBiome(this, rng, engine, x, y, z); } public KList getRealChildren(DataProvider g) { @@ -916,116 +572,31 @@ public class IrisBiome extends IrisRegistrant implements IRare { //TODO: Test public Biome getGroundBiome(RNG rng, double x, double y, double z) { - return getGroundBiome(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + return IrisBiomeDerivatives.getGroundBiome(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); } public Biome getGroundBiome(RNG rng, Engine engine, double x, double y, double z) { - if (biomeScatter.isEmpty()) { - return getDerivative(); - } - - if (biomeScatter.size() == 1) { - return getBiomeScatterResolved().get(0); - } - - return getBiomeGenerator(rng, engine).fit(getBiomeScatterResolved(), x, y, z); + return IrisBiomeDerivatives.getGroundBiome(this, rng, engine, x, y, z); } public String getSkyBiomeKey(RNG rng, double x, double y, double z) { - return getSkyBiomeKey(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + return IrisBiomeDerivatives.getSkyBiomeKey(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); } public String getSkyBiomeKey(RNG rng, Engine engine, double x, double y, double z) { - if (biomeSkyScatter.size() == 1) { - return namespacedBiomeKey(biomeSkyScatter.get(0)); - } - - if (biomeSkyScatter.isEmpty()) { - return getGroundBiomeKey(rng, engine, x, y, z); - } - - return namespacedBiomeKey(biomeSkyScatter.get(getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z))); + return IrisBiomeDerivatives.getSkyBiomeKey(this, rng, engine, x, y, z); } public String getGroundBiomeKey(RNG rng, double x, double y, double z) { - return getGroundBiomeKey(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); + return IrisBiomeDerivatives.getGroundBiomeKey(this, rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z); } public String getGroundBiomeKey(RNG rng, Engine engine, double x, double y, double z) { - if (biomeScatter.isEmpty()) { - return namespacedBiomeKey(derivative); - } - - if (biomeScatter.size() == 1) { - return namespacedBiomeKey(biomeScatter.get(0)); - } - - return namespacedBiomeKey(biomeScatter.get(getBiomeGenerator(rng, engine).fit(0, biomeScatter.size() - 1, x, y, z))); - } - - public PlatformBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) { - if (getLayers().isEmpty()) { - return B.getState("AIR"); - } - - return getLayers().get(0).get(rng, x, 0, z, idm); + return IrisBiomeDerivatives.getGroundBiomeKey(this, rng, engine, x, y, z); } public Color getColor(Engine engine, RenderType type) { - switch (type) { - case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> { - return this.cacheColor.aquire(() -> { - if (this.color == null) { - RandomColor randomColor = new RandomColor(getName().hashCode()); - String vanillaKey = this.getVanillaDerivativeKey(); - RandomColor.Color col = vanillaKey == null ? null : VanillaBiomeColors.getColorType(vanillaKey); - if (col == null) { - IrisLogging.warn("No vanilla biome found for " + getName()); - return new Color(randomColor.randomColor()); - } - RandomColor.Luminosity lum = VanillaBiomeColors.getColorLuminosity(vanillaKey); - RandomColor.SaturationType sat = VanillaBiomeColors.getColorSaturation(vanillaKey); - int newColorI = randomColor.randomColor(col, col == RandomColor.Color.MONOCHROME ? RandomColor.SaturationType.MONOCHROME : sat, lum); - - return new Color(newColorI); - } - - try { - return Color.decode(this.color); - } catch (NumberFormatException e) { - IrisLogging.warn("Could not parse color \"" + this.color + "\" for biome " + getName()); - return new Color(new RandomColor(getName().hashCode()).randomColor()); - } - }); - } - case OBJECT_LOAD -> { - return cacheColorObjectDensity.aquire(() -> { - double density = 0; - - for (IrisObjectPlacement i : getObjects()) { - density += i.getDensity() * i.getChance(); - } - - return Color.getHSBColor(0.225f, (float) (density / engine.getMaxBiomeObjectDensity()), 1f); - }); - } - case DECORATOR_LOAD -> { - return cacheColorDecoratorLoad.aquire(() -> { - double density = 0; - - for (IrisDecorator i : getDecorators()) { - density += i.getChance() * Math.min(1, i.getStackMax()) * 256; - } - - return Color.getHSBColor(0.41f, (float) (density / engine.getMaxBiomeDecoratorDensity()), 1f); - }); - } - case LAYER_LOAD -> { - return cacheColorLayerLoad.aquire(() -> Color.getHSBColor(0.625f, (float) (getLayers().size() / engine.getMaxBiomeLayerDensity()), 1f)); - } - } - - return Color.black; + return IrisBiomeColorRenderer.getColor(this, engine, type); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java new file mode 100644 index 000000000..6ba1918dd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java @@ -0,0 +1,93 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.data.VanillaBiomeColors; +import art.arcane.volmlib.util.inventorygui.RandomColor; + +import java.awt.Color; + +/** + * Map / render color resolution for {@link IrisBiome}. IrisBiome is Gson deserialized from pack + * JSON, so its fields stay put and only the behavior lives here. + */ +final class IrisBiomeColorRenderer { + private IrisBiomeColorRenderer() { + } + + static Color getColor(IrisBiome biome, Engine engine, RenderType type) { + switch (type) { + case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> { + return biome.getCacheColor().aquire(() -> { + if (biome.getColor() == null) { + RandomColor randomColor = new RandomColor(biome.getName().hashCode()); + String vanillaKey = biome.getVanillaDerivativeKey(); + RandomColor.Color col = vanillaKey == null ? null : VanillaBiomeColors.getColorType(vanillaKey); + if (col == null) { + IrisLogging.warn("No vanilla biome found for " + biome.getName()); + return new Color(randomColor.randomColor()); + } + RandomColor.Luminosity lum = VanillaBiomeColors.getColorLuminosity(vanillaKey); + RandomColor.SaturationType sat = VanillaBiomeColors.getColorSaturation(vanillaKey); + int newColorI = randomColor.randomColor(col, col == RandomColor.Color.MONOCHROME ? RandomColor.SaturationType.MONOCHROME : sat, lum); + + return new Color(newColorI); + } + + try { + return Color.decode(biome.getColor()); + } catch (NumberFormatException e) { + IrisLogging.warn("Could not parse color \"" + biome.getColor() + "\" for biome " + biome.getName()); + return new Color(new RandomColor(biome.getName().hashCode()).randomColor()); + } + }); + } + case OBJECT_LOAD -> { + return biome.getCacheColorObjectDensity().aquire(() -> { + double density = 0; + + for (IrisObjectPlacement i : biome.getObjects()) { + density += i.getDensity() * i.getChance(); + } + + return Color.getHSBColor(0.225f, (float) (density / engine.getMaxBiomeObjectDensity()), 1f); + }); + } + case DECORATOR_LOAD -> { + return biome.getCacheColorDecoratorLoad().aquire(() -> { + double density = 0; + + for (IrisDecorator i : biome.getDecorators()) { + density += i.getChance() * Math.min(1, i.getStackMax()) * 256; + } + + return Color.getHSBColor(0.41f, (float) (density / engine.getMaxBiomeDecoratorDensity()), 1f); + }); + } + case LAYER_LOAD -> { + return biome.getCacheColorLayerLoad().aquire(() -> Color.getHSBColor(0.625f, (float) (biome.getLayers().size() / engine.getMaxBiomeLayerDensity()), 1f)); + } + } + + return Color.black; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java new file mode 100644 index 000000000..6c61dcdb6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeDerivatives.java @@ -0,0 +1,152 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Biome; + +/** + * Vanilla derivative and biome scatter resolution for {@link IrisBiome}. IrisBiome is Gson + * deserialized from pack JSON, so its fields stay put and only the behavior lives here. + */ +final class IrisBiomeDerivatives { + private IrisBiomeDerivatives() { + } + + static String namespacedBiomeKey(String key) { + if (key == null || key.isBlank()) { + return null; + } + String trimmed = key.trim(); + return trimmed.indexOf(':') >= 0 ? trimmed : "minecraft:" + trimmed; + } + + static String getVanillaDerivativeKey(String derivative, String vanillaDerivative) { + String resolved = namespacedBiomeKey(vanillaDerivative); + return resolved == null ? namespacedBiomeKey(derivative) : resolved; + } + + static String getStructureDerivativeKey(IrisBiome biome) { + String key = biome.getVanillaDerivativeKey(); + if (key == null || !key.startsWith("minecraft:")) { + return key; + } + if (biome.isSea() && !isVanillaSeaStructureBiome(key)) { + return "minecraft:the_void"; + } + if (biome.isShore() && !isVanillaShoreStructureBiome(key)) { + return "minecraft:beach"; + } + return key; + } + + static boolean isVanillaSeaStructureBiome(String key) { + return key != null && (key.contains("ocean") || key.endsWith("river")); + } + + static boolean isVanillaShoreStructureBiome(String key) { + return key != null && (key.endsWith("beach") || key.endsWith("shore")); + } + + static KList resolveBiomeKeys(KList keys) { + KList resolved = new KList<>(); + for (String key : keys) { + resolved.add(resolveBiomeKey(key)); + } + return resolved; + } + + static Biome resolveBiomeKey(String key) { + if (key == null) { + return null; + } + NamespacedKey namespacedKey = NamespacedKey.fromString(key); + return namespacedKey == null ? null : RegistryUtil.lookup(Biome.class).get(namespacedKey); + } + + static Biome getSkyBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeSkyScatter = biome.getBiomeSkyScatter(); + + if (biomeSkyScatter.size() == 1) { + return biome.getBiomeSkyScatterResolved().get(0); + } + + if (biomeSkyScatter.isEmpty()) { + return getGroundBiome(biome, rng, engine, x, y, z); + } + + return biome.getBiomeSkyScatterResolved().get(biome.getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z)); + } + + static Biome getGroundBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeScatter = biome.getBiomeScatter(); + + if (biomeScatter.isEmpty()) { + return biome.getDerivative(); + } + + if (biomeScatter.size() == 1) { + return biome.getBiomeScatterResolved().get(0); + } + + return biome.getBiomeGenerator(rng, engine).fit(biome.getBiomeScatterResolved(), x, y, z); + } + + static String getSkyBiomeKey(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeSkyScatter = biome.getBiomeSkyScatter(); + + if (biomeSkyScatter.size() == 1) { + return namespacedBiomeKey(biomeSkyScatter.get(0)); + } + + if (biomeSkyScatter.isEmpty()) { + return getGroundBiomeKey(biome, rng, engine, x, y, z); + } + + return namespacedBiomeKey(biomeSkyScatter.get(biome.getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z))); + } + + static String getGroundBiomeKey(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList biomeScatter = biome.getBiomeScatter(); + + if (biomeScatter.isEmpty()) { + return biome.getDerivativeKey(); + } + + if (biomeScatter.size() == 1) { + return namespacedBiomeKey(biomeScatter.get(0)); + } + + return namespacedBiomeKey(biomeScatter.get(biome.getBiomeGenerator(rng, engine).fit(0, biomeScatter.size() - 1, x, y, z))); + } + + static IrisBiomeCustom getCustomBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) { + KList customDerivitives = biome.getCustomDerivitives(); + + if (customDerivitives.size() == 1) { + return customDerivitives.get(0); + } + + return customDerivitives.get(biome.getBiomeGenerator(rng, engine).fit(0, customDerivitives.size() - 1, x, y, z)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java new file mode 100644 index 000000000..08371424f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGenLinks.java @@ -0,0 +1,137 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.volmlib.util.collection.KMap; + +/** + * Generator link lookups for {@link IrisBiome}. IrisBiome is Gson deserialized from pack JSON, so + * its fields stay put and only the behavior lives here. These are resolved per generation column, + * so the caches are read through {@link AtomicCache#getIfPresent()} first. + */ +final class IrisBiomeGenLinks { + private IrisBiomeGenLinks() { + } + + static double getGenLinkMax(IrisBiome biome, String loadKey, Engine engine) { + if (loadKey == null || loadKey.isBlank()) { + return 0; + } + + Integer v = maxIndex(biome).get(loadKey); + + return v == null ? 0 : v; + } + + static double getGenLinkMin(IrisBiome biome, String loadKey, Engine engine) { + if (loadKey == null || loadKey.isBlank()) { + return 0; + } + + Integer v = minIndex(biome).get(loadKey); + + return v == null ? 0 : v; + } + + static IrisBiomeGeneratorLink getGenLink(IrisBiome biome, String loadKey) { + if (loadKey == null || loadKey.isBlank()) { + return null; + } + + return linkIndex(biome).get(loadKey); + } + + private static KMap maxIndex(IrisBiome biome) { + AtomicCache> cache = biome.getGenCacheMax(); + KMap cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KMap l = new KMap<>(); + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i.getMax()); + + } + + return l; + }); + } + + private static KMap minIndex(IrisBiome biome) { + AtomicCache> cache = biome.getGenCacheMin(); + KMap cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KMap l = new KMap<>(); + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i.getMin()); + } + + return l; + }); + } + + private static KMap linkIndex(IrisBiome biome) { + AtomicCache> cache = biome.getGenCache(); + KMap cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KMap l = new KMap<>(); + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + String generatorKey = i.getGenerator(); + if (generatorKey == null || generatorKey.isBlank()) { + continue; + } + + l.put(generatorKey, i); + } + + return l; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java index ed27ff39b..77881b1bb 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeGeneratorLink.java @@ -47,14 +47,14 @@ public class IrisBiomeGeneratorLink { private String generator = "default"; @DependsOn({"min", "max"}) @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MinNumber(-2032) + @MaxNumber(2032) @Desc("The min block value (value + fluidHeight)") private int min = 0; @DependsOn({"min", "max"}) @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MinNumber(-2032) + @MaxNumber(2032) @Desc("The max block value (value + fluidHeight)") private int max = 0; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java new file mode 100644 index 000000000..3103aad37 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeLayerGenerator.java @@ -0,0 +1,356 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +/** + * Layer (block palette) generation for {@link IrisBiome}. IrisBiome is Gson deserialized from pack + * JSON, so its fields stay put and only the behavior lives here. + */ +final class IrisBiomeLayerGenerator { + private static final class States { + private static final PlatformBlockState BARRIER = B.getState("BARRIER"); + } + + private IrisBiomeLayerGenerator() { + } + + static KList generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { + if (biome.isLockLayers()) { + return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex); + } + + KList data = new KList<>(); + + if (maxDepth <= 0) { + return data; + } + + KList layers = biome.getLayers(); + int layerCount = layers.size(); + + if (layerCount <= 0) { + return data; + } + + KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = layers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + IrisSlopeClip sc = layer.getSlopeCondition(); + + if (!sc.isDefault()) { + if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { + d = 0; + } + } + + if (d <= 0) { + continue; + } + + for (int j = 0; j < d; j++) { + if (data.size() >= maxDepth) { + break; + } + + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (data.size() >= maxDepth) { + break; + } + + if (dim.isExplodeBiomePalettes()) { + for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { + data.add(States.BARRIER); + + if (data.size() >= maxDepth) { + break; + } + } + } + } + + return data; + } + + static KList generateCeilingLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { + KList data = new KList<>(); + + if (maxDepth <= 0) { + return data; + } + + KList ceilingLayers = biome.getCaveCeilingLayers(); + int layerCount = ceilingLayers.size(); + + if (layerCount <= 0) { + return data; + } + + KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = ceilingLayers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + if (d <= 0) { + continue; + } + + for (int j = 0; j < d; j++) { + if (data.size() >= maxDepth) { + break; + } + + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (data.size() >= maxDepth) { + break; + } + + if (dim.isExplodeBiomePalettes()) { + for (int j = 0; j < dim.getExplodeBiomePaletteSize(); j++) { + data.add(States.BARRIER); + + if (data.size() >= maxDepth) { + break; + } + } + } + } + + return data; + } + + static KList generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) { + KList data = new KList<>(); + KList real = new KList<>(); + int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax()); + if (maxDepth <= 0) { + return data; + } + + KList layers = biome.getLayers(); + int layerCount = layers.size(); + + if (layerCount > 0) { + KList heightGenerators = getLayerHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = layers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + IrisSlopeClip sc = layer.getSlopeCondition(); + + if (!sc.isDefault()) { + if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) { + d = 0; + } + } + + if (d <= 0) { + continue; + } + + for (int j = 0; j < d; j++) { + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + } + } + + if (data.isEmpty()) { + return real; + } + + for (int i = 0; i < maxDepth; i++) { + int offset = (512 - height) - i; + int index = offset % data.size(); + real.add(data.get(Math.max(index, 0))); + } + + return real; + } + + static KList generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) { + KList data = new KList<>(); + + KList seaLayers = biome.getSeaLayers(); + int layerCount = seaLayers.size(); + + if (layerCount <= 0) { + return data; + } + + KList heightGenerators = getLayerSeaHeightGenerators(biome, random, rdata); + + for (int i = 0; i < layerCount; i++) { + IrisBiomePaletteLayer layer = seaLayers.get(i); + double zoom = layer.getZoom(); + CNG hgen = heightGenerators.get(i); + int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom); + + if (d < 0) { + continue; + } + + for (int j = 0; j < d; j++) { + if (data.size() >= maxDepth) { + break; + } + + try { + data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata)); + } catch (Throwable e) { + IrisLogging.reportError(e); + e.printStackTrace(); + } + } + + if (data.size() >= maxDepth) { + break; + } + } + + return data; + } + + static KList getLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData rdata) { + AtomicCache> cache = biome.getLayerHeightGenerators(); + KList cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KList layerHeightGenerators = new KList<>(); + + int m = 7235; + + for (IrisBiomePaletteLayer i : biome.getLayers()) { + layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata)); + } + + return layerHeightGenerators; + }); + } + + static KList getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) { + AtomicCache> cache = biome.getLayerSeaHeightGenerators(); + KList cached = cache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return cache.aquire(() -> + { + KList layerSeaHeightGenerators = new KList<>(); + + int m = 7735; + + for (IrisBiomePaletteLayer i : biome.getSeaLayers()) { + layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data)); + } + + return layerSeaHeightGenerators; + }); + } + + static PlatformBlockState getSurfaceBlock(IrisBiome biome, int x, int z, RNG rng, IrisData idm) { + KList layers = biome.getLayers(); + + if (layers.isEmpty()) { + return B.getState("AIR"); + } + + return layers.get(0).get(rng, x, 0, z, idm); + } + + static int getMaxHeight(IrisBiome biome, Engine engine) { + return biome.getMaxHeight().aquire(() -> + { + int maxHeight = 0; + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + maxHeight += i.getMax(); + } + + return maxHeight; + }); + } + + static int getMaxWithObjectHeight(IrisBiome biome, IrisData data, Engine engine) { + return biome.getMaxWithObjectHeight().aquire(() -> + { + int maxHeight = 0; + + for (IrisBiomeGeneratorLink i : biome.getGenerators()) { + maxHeight += i.getMax(); + } + + int gg = 0; + + for (IrisObjectPlacement i : biome.getObjects()) { + for (IrisObject j : data.getObjectLoader().loadAll(i.getPlace())) { + gg = Math.max(gg, j.getH()); + } + } + + return maxHeight + gg + 3; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java new file mode 100644 index 000000000..39c6ac15a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeOres.java @@ -0,0 +1,119 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; + +/** + * Ore generation for {@link IrisBiome}. IrisBiome is Gson deserialized from pack JSON, so its + * fields stay put and only the behavior lives here. Note that {@code IrisBiome.setOres(KList)} + * stays on IrisBiome: it is a hand written Lombok setter override that resets these caches. + */ +final class IrisBiomeOres { + private IrisBiomeOres() { + } + + static PlatformBlockState generateOres(IrisBiome biome, int x, int y, int z, RNG rng, IrisData data, boolean surface) { + KList localOres = surface ? getSurfaceOres(biome) : getUndergroundOres(biome); + return generateOres(localOres, x, y, z, rng, data); + } + + static PlatformBlockState generateSurfaceOres(IrisBiome biome, int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getSurfaceOres(biome), x, y, z, rng, data); + } + + static PlatformBlockState generateUndergroundOres(IrisBiome biome, int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getUndergroundOres(biome), x, y, z, rng, data); + } + + static boolean hasSurfaceOres(IrisBiome biome) { + return !getSurfaceOres(biome).isEmpty(); + } + + static boolean hasUndergroundOres(IrisBiome biome) { + return !getUndergroundOres(biome).isEmpty(); + } + + static KList getSurfaceOreGenerators(IrisBiome biome) { + return getOres(biome, true); + } + + static KList getUndergroundOreGenerators(IrisBiome biome) { + return getOres(biome, false); + } + + static IrisOreGeneratorBounds getSurfaceOreGeneratorBounds(IrisBiome biome) { + return biome.getSurfaceOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres(biome))); + } + + static IrisOreGeneratorBounds getUndergroundOreGeneratorBounds(IrisBiome biome) { + return biome.getUndergroundOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres(biome))); + } + + private static KList getSurfaceOres(IrisBiome biome) { + return getOres(biome, true); + } + + private static KList getUndergroundOres(IrisBiome biome) { + return getOres(biome, false); + } + + private static KList getOres(IrisBiome biome, boolean surface) { + AtomicCache> oreCache = surface ? biome.getSurfaceOreCache() : biome.getUndergroundOreCache(); + KList cached = oreCache.getIfPresent(); + + if (cached != null) { + return cached; + } + + return oreCache.aquire(() -> { + KList filtered = new KList<>(); + KList localOres = biome.getOres(); + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + if (oreGenerator.isGenerateSurface() == surface) { + filtered.add(oreGenerator); + } + } + + return filtered; + }); + } + + private static PlatformBlockState generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { + if (localOres.isEmpty()) { + return null; + } + + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + PlatformBlockState ore = oreGenerator.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + return null; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java index db3e455b3..4087c28a6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java @@ -50,13 +50,13 @@ public class IrisBiomePaletteLayer { private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); @DependsOn({"minHeight", "maxHeight"}) @MinNumber(0) - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MaxNumber(2032) @Desc("The min thickness of this layer") private int minHeight = 1; @DependsOn({"minHeight", "maxHeight"}) @MinNumber(1) - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MaxNumber(2032) @Desc("The max thickness of this layer") private int maxHeight = 1; @@ -71,28 +71,56 @@ public class IrisBiomePaletteLayer { private KList palette = new KList().qadd(new IrisBlockData("GRASS_BLOCK")); public CNG getHeightGenerator(RNG rng, IrisData data) { + CNG cached = heightGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + return heightGenerator.aquire(() -> CNG.signature(rng.nextParallelRNG(minHeight * maxHeight + getBlockData(data).size()))); } public PlatformBlockState get(RNG rng, double x, double y, double z, IrisData data) { - if (getBlockData(data).isEmpty()) { + return get(rng, 0, x, y, z, data); + } + + /** + * Resolves a block for this layer without allocating a child RNG per call. The child RNG is + * only built inside the lazy layer generator initializer, using the exact same seed derivation + * as {@code parent.nextParallelRNG(signature)} followed by the generator signature. + */ + public PlatformBlockState get(RNG parent, int signature, double x, double y, double z, IrisData data) { + KList localBlockData = getBlockData(data); + + if (localBlockData.isEmpty()) { return null; } - if (getBlockData(data).size() == 1) { - return getBlockData(data).get(0); + if (localBlockData.size() == 1) { + return localBlockData.get(0); } - double scaledX = x / zoom; - double scaledY = y / zoom; - double scaledZ = z / zoom; - return getLayerGenerator(rng, data).fit(getBlockData(data), scaledX, scaledY, scaledZ); + double localZoom = zoom; + double scaledX = x / localZoom; + double scaledY = y / localZoom; + double scaledZ = z / localZoom; + return getLayerGenerator(parent, signature, data).fit(localBlockData, scaledX, scaledY, scaledZ); } public CNG getLayerGenerator(RNG rng, IrisData data) { + return getLayerGenerator(rng, 0, data); + } + + public CNG getLayerGenerator(RNG parent, int signature, IrisData data) { + CNG cached = layerGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + return layerGenerator.aquire(() -> { - RNG rngx = rng.nextParallelRNG(minHeight + maxHeight + getBlockData(data).size()); + RNG rngx = parent.nextParallelRNG(signature).nextParallelRNG(minHeight + maxHeight + getBlockData(data).size()); return style.create(rngx, data); }); } @@ -104,6 +132,12 @@ public class IrisBiomePaletteLayer { } public KList getBlockData(IrisData data) { + KList cached = blockData.getIfPresent(); + + if (cached != null) { + return cached; + } + return blockData.aquire(() -> { KList blockData = new KList<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java deleted file mode 100644 index 34279a511..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveShape.java +++ /dev/null @@ -1,85 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.util.common.math.IrisBlockVector; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.iris.engine.object.annotations.Snippet; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.collection.KSet; -import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("cave-shape") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Cave Shape") -@Data -public class IrisCaveShape { - private transient final KMap> cache = new KMap<>(); - - @Desc("Noise used for the shape of the cave") - private IrisGeneratorStyle noise = new IrisGeneratorStyle(); - @MinNumber(0) - @MaxNumber(1) - @Desc("The threshold for noise mask") - private double noiseThreshold = -1; - - @RegistryListResource(IrisObject.class) - @Desc("Object used as mask for the shape of the cave") - private String object = null; - @Desc("Rotation to apply to objects before using them as mask") - private IrisObjectRotation objectRotation = new IrisObjectRotation(); - - public CNG getNoise(RNG rng, Engine engine) { - return noise.create(rng, engine.getData()); - } - - public KSet getMasked(RNG rng, Engine engine) { - if (object == null) return null; - return cache.computeIfAbsent(randomRotation(rng), pos -> { - var rotated = new KSet(); - engine.getData().getObjectLoader().load(object).getBlocks().forEach((vector, data) -> { - if (data.isAir()) return; - IrisBlockVector rot = objectRotation.rotate(vector, pos.getX(), pos.getY(), pos.getZ()); - rotated.add(new IrisPosition(rot.getBlockX(), rot.getBlockY(), rot.getBlockZ())); - }); - return rotated; - }); - } - - private IrisPosition randomRotation(RNG rng) { - if (objectRotation == null || !objectRotation.canRotate()) - return new IrisPosition(0,0,0); - return new IrisPosition( - randomDegree(rng, objectRotation.getXAxis()), - randomDegree(rng, objectRotation.getYAxis()), - randomDegree(rng, objectRotation.getZAxis()) - ); - } - - private int randomDegree(RNG rng, IrisAxisRotationClamp clamp) { - if (!clamp.isEnabled()) return 0; - if (clamp.isLocked()) return (int) clamp.getMax(); - double interval = clamp.getInterval(); - if (interval < 1) interval = 1; - - double min = clamp.getMin(), max = clamp.getMax(); - double value = (interval * (Math.ceil(Math.abs(rng.d(0, 360) / interval)))) % 360D; - if (clamp.isUnlimited()) return (int) value; - - if (min > max) { - max = clamp.getMin(); - min = clamp.getMax(); - } - return (int) (double) M.clip(value, min, max); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java index 66cb4a39e..2e953697c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDecorator.java @@ -51,6 +51,8 @@ public class IrisDecorator { private final transient AtomicCache> blockDataTops = new AtomicCache<>(); private final transient AtomicCache blockDataArray = new AtomicCache<>(); private final transient AtomicCache blockDataTopsArray = new AtomicCache<>(); + private final transient AtomicCache whitelistArray = new AtomicCache<>(); + private final transient AtomicCache blacklistArray = new AtomicCache<>(); @Desc("The varience dispersion is used when multiple blocks are put in the palette. Scatter scrambles them, Wispy shows streak-looking varience") private IrisGeneratorStyle variance = NoiseStyle.STATIC.style(); @Desc("Forcefully place this decorant anywhere it is supposed to go even if it should not go on a specific surface block. For example, you could force tallgrass to place on top of stone by using this.") @@ -77,12 +79,12 @@ public class IrisDecorator { private IrisDecorationPart partOf = IrisDecorationPart.NONE; @DependsOn({"stackMin", "stackMax"}) @MinNumber(1) - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MaxNumber(2032) @Desc("The minimum repeat stack height (setting to 3 would stack 3 of on top of each other") private int stackMin = 1; @DependsOn({"stackMin", "stackMax"}) @MinNumber(1) - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MaxNumber(2032) @Desc("The maximum repeat stack height") private int stackMax = 1; @DependsOn({"stackMin", "stackMax"}) @@ -95,7 +97,6 @@ public class IrisDecorator { @MinNumber(0) @MaxNumber(1) @Desc("The chance for this decorator to decorate at a given X,Y coordinate. This is hit 256 times per chunk (per surface block)") - // TODO: WARNING HEIGHT private double chance = 0.1; @Required @ArrayType(min = 1, type = IrisBlockData.class) @@ -122,15 +123,33 @@ public class IrisDecorator { } public CNG getHeightGenerator(RNG rng, IrisData data) { + CNG cached = heightGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + return heightGenerator.aquire(() -> heightVariance.create(rng.nextParallelRNG(getBlockData(data).size() + stackMax + stackMin), data)); } public CNG getGenerator(RNG rng, IrisData data) { + CNG cached = layerGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + return layerGenerator.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data)); } public CNG getVarianceGenerator(RNG rng, IrisData data) { + CNG cached = varianceGenerator.getIfPresent(); + + if (cached != null) { + return cached; + } + return varianceGenerator.aquire(() -> variance.create( rng.nextParallelRNG(getBlockData(data).size()), data) @@ -167,16 +186,6 @@ public class IrisDecorator { return null; } - double xx = x; - double yy = y; - double zz = z; - - if (!getVarianceGenerator(rng, data).isStatic()) { - xx = x / style.getZoom(); - yy = y / style.getZoom(); - zz = z / style.getZoom(); - } - if (getBlockData(data).size() == 1) { return getBlockData(data).get(0); } @@ -204,6 +213,12 @@ public class IrisDecorator { } public KList getBlockData(IrisData data) { + KList cached = blockData.getIfPresent(); + + if (cached != null) { + return cached; + } + return blockData.aquire(() -> { KList blockData = new KList<>(); @@ -221,6 +236,12 @@ public class IrisDecorator { } public KList getBlockDataTops(IrisData data) { + KList cached = blockDataTops.getIfPresent(); + + if (cached != null) { + return cached; + } + return blockDataTops.aquire(() -> { KList blockDataTops = new KList<>(); @@ -238,6 +259,12 @@ public class IrisDecorator { } public PlatformBlockState[] getBlockDataArray(IrisData data) { + PlatformBlockState[] cached = blockDataArray.getIfPresent(); + + if (cached != null) { + return cached; + } + return blockDataArray.aquire(() -> { KList list = getBlockData(data); return list.toArray(new PlatformBlockState[0]); @@ -245,12 +272,56 @@ public class IrisDecorator { } public PlatformBlockState[] getBlockDataTopsArray(IrisData data) { + PlatformBlockState[] cached = blockDataTopsArray.getIfPresent(); + + if (cached != null) { + return cached; + } + return blockDataTopsArray.aquire(() -> { KList list = getBlockDataTops(data); return list.toArray(new PlatformBlockState[0]); }); } + /** + * The resolved whitelist palette. Empty when no whitelist is configured; callers must still gate on + * {@link #getWhitelist()} being non null, because an explicitly empty whitelist blocks all placement. + */ + public PlatformBlockState[] getWhitelistArray(IrisData data) { + PlatformBlockState[] cached = whitelistArray.getIfPresent(); + + if (cached != null) { + return cached; + } + + return whitelistArray.aquire(() -> resolvePalette(whitelist, data)); + } + + public PlatformBlockState[] getBlacklistArray(IrisData data) { + PlatformBlockState[] cached = blacklistArray.getIfPresent(); + + if (cached != null) { + return cached; + } + + return blacklistArray.aquire(() -> resolvePalette(blacklist, data)); + } + + private static PlatformBlockState[] resolvePalette(KList list, IrisData data) { + if (list == null) { + return new PlatformBlockState[0]; + } + + PlatformBlockState[] resolved = new PlatformBlockState[list.size()]; + + for (int i = 0; i < resolved.length; i++) { + resolved[i] = list.get(i).getBlockData(data); + } + + return resolved; + } + public PlatformBlockState pickBlockData(RNG rng, IrisData data, double x, double z) { PlatformBlockState[] arr = getBlockDataArray(data); if (arr.length == 0) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java index 0ebd49d00..41204f865 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java @@ -54,12 +54,12 @@ public class IrisDepositGenerator { private final transient ConcurrentMap> scaledObjects = new ConcurrentHashMap<>(); @Required @MinNumber(0) - @MaxNumber(8192) // TODO: WARNING HEIGHT + @MaxNumber(8192) @Desc("The minimum height this deposit can generate at") private int minHeight = 1; @Required @MinNumber(0) - @MaxNumber(8192) // TODO: WARNING HEIGHT + @MaxNumber(8192) @Desc("The maximum height this deposit can generate at") private int maxHeight = 75; @Required diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java index beeb45fc4..286d6cf4b 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java @@ -301,9 +301,8 @@ public class IrisEffect { if (particleType != null) { Location part = p.getLocation().clone().add(0, 0.25, 0).add(new Vector(1, 1, 1).multiply(RNG.r.d())).subtract(new Vector(1, 1, 1).multiply(RNG.r.d())); part.add(RNG.r.d(), 0, RNG.r.d()); - int offset = p.getWorld().getMinHeight(); if (extra != 0) { - p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), + p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + RNG.r.i(particleOffset), part.getZ(), particleCount, randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, @@ -311,7 +310,7 @@ public class IrisEffect { randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ, extra); } else { - p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(), + p.getWorld().spawnParticle(particleType, part.getX(), part.getY() + RNG.r.i(particleOffset), part.getZ(), particleCount, randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX, randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY, diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFontStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFontStyle.java deleted file mode 100644 index d84255e57..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisFontStyle.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Represents a basic font style to apply to a font family") -public enum IrisFontStyle { - @Desc("Plain old text") - PLAIN, - - @Desc("Italicized Text") - ITALIC, - - @Desc("Bold Text") - BOLD, -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java index f1f72f0c9..93d0d8d6c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGenerator.java @@ -103,48 +103,6 @@ public class IrisGenerator extends IrisRegistrant { return cellGen.aquire(() -> new CellGenerator(new RNG(seed + 239466))); } - public T fitRarity(KList b, long superSeed, double rx, double rz) { - if (b.size() == 0) { - return null; - } - - if (b.size() == 1) { - return b.get(0); - } - - KList rarityMapped = new KList<>(); - boolean o = false; - int max = 1; - for (T i : b) { - if (i.getRarity() > max) { - max = i.getRarity(); - } - } - - max++; - - for (T i : b) { - for (int j = 0; j < max - i.getRarity(); j++) { - //noinspection AssignmentUsedAsCondition - if (o = !o) { - rarityMapped.add(i); - } else { - rarityMapped.add(0, i); - } - } - } - - if (rarityMapped.size() == 1) { - return rarityMapped.get(0); - } - - if (rarityMapped.isEmpty()) { - throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); - } - - return fit(rarityMapped, superSeed, rx, rz); - } - public T fit(T[] v, long superSeed, double rx, double rz) { if (v.length == 0) { return null; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java index 000fd2e5a..868126742 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImage.java @@ -28,6 +28,7 @@ import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.util.Objects; public class IrisImage extends IrisRegistrant { private final BufferedImage image; @@ -37,7 +38,7 @@ public class IrisImage extends IrisRegistrant { } public IrisImage(BufferedImage image) { - this.image = image; + this.image = Objects.requireNonNull(image, "IrisImage requires a decoded image (the source file was unreadable or not an image)"); } public int getWidth() { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java index f2607cfd9..e7f7f3f9f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java @@ -25,15 +25,13 @@ import art.arcane.iris.engine.object.annotations.Required; import art.arcane.volmlib.util.function.NoiseProvider; import art.arcane.iris.util.project.interpolation.InterpolationMethod; import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBoundsProvider; +import art.arcane.iris.util.project.interpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import java.util.Objects; - @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor @@ -54,7 +52,11 @@ public class IrisInterpolator { @Override public int hashCode() { - return Objects.hash(horizontalScale, function); + // Bit-identical to Objects.hash(horizontalScale, function) without the Object[] + Double boxing. + // The exact value is load bearing: it decides HashMap bucket order for the generator maps in + // IrisComplex, and that order fixes the floating point summation order of interpolated heights. + int result = 31 + Double.hashCode(horizontalScale); + return (31 * result) + (function == null ? 0 : function.hashCode()); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java deleted file mode 100644 index 1838dc3eb..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator3D.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.Required; -import art.arcane.iris.engine.object.annotations.Snippet; -import art.arcane.volmlib.util.function.NoiseProvider3; -import art.arcane.iris.util.project.interpolation.InterpolationMethod3D; -import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("interpolator-3d") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Configures interpolatin in 3D") -@Data -public class IrisInterpolator3D { - @Required - @Desc("The interpolation method when two biomes use different heights but this same generator") - private InterpolationMethod3D function = InterpolationMethod3D.TRILINEAR; - - @Required - @MinNumber(1) - @MaxNumber(8192) - @Desc("The range checked in all dimensions. Smaller ranges yeild more detail but are not as smooth.") - private double scale = 4; - - public double interpolate(double x, double y, double z, NoiseProvider3 provider) { - return interpolate((int) Math.round(x), (int) Math.round(y), (int) Math.round(z), provider); - } - - public double interpolate(int x, int y, int z, NoiseProvider3 provider) { - return IrisInterpolation.getNoise3D(getFunction(), x, y, z, getScale(), provider); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index ba79ac2f3..2c037a9c4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -18,40 +18,23 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.framework.PlacedObject; -import art.arcane.iris.engine.framework.placer.HeightmapObjectPlacer; import art.arcane.iris.platform.bukkit.BukkitBlockState; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.VectorMap; -import art.arcane.volmlib.util.format.Form; -import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import art.arcane.iris.util.project.noise.SimplexNoise; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.volmlib.util.math.BlockPosition; -import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.common.math.AxisAlignedBB; import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.common.math.IrisVector; import art.arcane.iris.util.common.math.Vector3i; -import art.arcane.volmlib.util.matter.MatterMarker; -import art.arcane.iris.util.common.parallel.BurstExecutor; -import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.common.plugin.VolmitSender; -import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import art.arcane.iris.util.common.scheduling.jobs.Job; -import art.arcane.iris.engine.IrisComplex; -import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.RNG; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -60,28 +43,25 @@ import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; -import java.util.stream.StreamSupport; +/** + * A voxel volume loaded from a .iob file. Instances are loader cached and shared across generation threads, so all + * volume access goes through the read/write lock pair below. + *

+ * The heavy behaviour lives in package-private collaborators that read this state directly: + * {@link IrisObjectIO} (binary persistence), {@link IrisObjectShaping} (boring, shrinkwrap, block classification), + * {@link IrisObjectTransforms} (rotation, scaling, interpolated upscaling) and {@link IrisObjectPlacementRunner} + * (world placement). + */ @Accessors(chain = true) @EqualsAndHashCode(callSuper = false) public class IrisObject extends IrisRegistrant { @@ -93,9 +73,6 @@ public class IrisObject extends IrisRegistrant { static final PlatformBlockState VAIR_DEBUG = B.getState("COBWEB"); static final PlatformBlockState[] SNOW_LAYERS = new PlatformBlockState[]{B.getState("minecraft:snow[layers=1]"), B.getState("minecraft:snow[layers=2]"), B.getState("minecraft:snow[layers=3]"), B.getState("minecraft:snow[layers=4]"), B.getState("minecraft:snow[layers=5]"), B.getState("minecraft:snow[layers=6]"), B.getState("minecraft:snow[layers=7]"), B.getState("minecraft:snow[layers=8]")}; } - private static final long IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS = 5000L; - private static final long VACUUM_WAVE_SEED = 7392113L; - private static final java.util.concurrent.ConcurrentHashMap IMPLAUSIBLE_BEDROCK_WARNS = new java.util.concurrent.ConcurrentHashMap<>(); protected transient final Lock readLock; protected transient final Lock writeLock; @Getter @@ -103,25 +80,25 @@ public class IrisObject extends IrisRegistrant { protected transient volatile boolean smartBored = false; @Setter protected transient AtomicCache aabb = new AtomicCache<>(); - private transient final AtomicCache> surfaceSupportOffsets = new AtomicCache<>(); + transient final AtomicCache> surfaceSupportOffsets = new AtomicCache<>(); @Getter - private VectorMap blocks; + VectorMap blocks; @Getter - private VectorMap states; + VectorMap states; @Getter @Setter - private int w; + int w; @Getter @Setter - private int d; + int d; @Getter @Setter - private int h; + int h; @Getter @Setter - private transient Vector3i center; + transient Vector3i center; @Getter - private transient Vector3i shrinkOffset; + transient Vector3i shrinkOffset; public IrisObject(int w, int h, int d) { blocks = new VectorMap<>(); @@ -149,172 +126,13 @@ public class IrisObject extends IrisRegistrant { } public static IrisBlockVector sampleSize(File file) throws IOException { - try (DataInputStream din = new DataInputStream(new FileInputStream(file))) { - return new IrisBlockVector(din.readInt(), din.readInt(), din.readInt()); - } - } - - private static List blocksBetweenTwoPoints(IrisVector loc1, IrisVector loc2) { - List locations = new ArrayList<>(); - int topBlockX = Math.max(loc1.getBlockX(), loc2.getBlockX()); - int bottomBlockX = Math.min(loc1.getBlockX(), loc2.getBlockX()); - int topBlockY = Math.max(loc1.getBlockY(), loc2.getBlockY()); - int bottomBlockY = Math.min(loc1.getBlockY(), loc2.getBlockY()); - int topBlockZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); - int bottomBlockZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ()); - - for (int x = bottomBlockX; x <= topBlockX; x++) { - for (int z = bottomBlockZ; z <= topBlockZ; z++) { - for (int y = bottomBlockY; y <= topBlockY; y++) { - locations.add(new IrisBlockVector(x, y, z)); - } - } - } - return locations; - } - - private static boolean shouldStilt(PlatformBlockState state) { - if (!state.isOccluding()) { - return false; - } - String material = materialKey(state); - if (material.endsWith("_stairs") || material.endsWith("_slab")) { - return false; - } - return !material.equals("minecraft:dirt_path"); - } - - private static String materialKey(PlatformBlockState state) { - return IrisProceduralBlocks.materialKey(state); + return IrisObjectIO.sampleSize(file); } public AxisAlignedBB getAABB() { return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d))); } - public void ensureSmartBored(boolean debug) { - if (smartBored) { - return; - } - - PrecisionStopwatch p = PrecisionStopwatch.start(); - PlatformBlockState vair = debug ? States.VAIR_DEBUG : States.VAIR; - writeLock.lock(); - AtomicInteger applied = new AtomicInteger(); - if (blocks.isEmpty()) { - writeLock.unlock(); - IrisLogging.warn("Cannot Smart Bore " + getLoadKey() + " because it has 0 blocks in it."); - smartBored = true; - return; - } - - IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); - IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); - - for (IrisBlockVector i : blocks.keys()) { - max.setX(Math.max(i.getX(), max.getX())); - min.setX(Math.min(i.getX(), min.getX())); - max.setY(Math.max(i.getY(), max.getY())); - min.setY(Math.min(i.getY(), min.getY())); - max.setZ(Math.max(i.getZ(), max.getZ())); - min.setZ(Math.min(i.getZ(), min.getZ())); - } - - BurstExecutor burst = MultiBurst.burst.burst(); - - // Smash X - for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { - int finalRayY = rayY; - burst.queue(() -> { - for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { - if (blocks.containsKey(new IrisBlockVector(ray, finalRayY, rayZ))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - IrisBlockVector v = new IrisBlockVector(i, finalRayY, rayZ); - - if (!vair.equals(blocks.get(v))) { - blocks.computeIfAbsent(v, (vv) -> vair); - applied.getAndIncrement(); - } - } - } - } - }); - } - - // Smash Y - for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { - int finalRayX = rayX; - burst.queue(() -> { - for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { - if (blocks.containsKey(new IrisBlockVector(finalRayX, ray, rayZ))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - IrisBlockVector v = new IrisBlockVector(finalRayX, i, rayZ); - - if (!vair.equals(blocks.get(v))) { - blocks.computeIfAbsent(v, (vv) -> vair); - applied.getAndIncrement(); - } - } - } - } - }); - } - - // Smash Z - for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { - int finalRayX = rayX; - burst.queue(() -> { - for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { - if (blocks.containsKey(new IrisBlockVector(finalRayX, rayY, ray))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - IrisBlockVector v = new IrisBlockVector(finalRayX, rayY, i); - - if (!vair.equals(blocks.get(v))) { - blocks.computeIfAbsent(v, (vv) -> vair); - applied.getAndIncrement(); - } - } - } - } - }); - } - - burst.complete(); - smartBored = true; - writeLock.unlock(); - IrisLogging.debug("Smart Bore: " + getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")"); - } - public synchronized IrisObject copy() { IrisObject o = new IrisObject(w, h, d); o.setLoadKey(getLoadKey()); @@ -329,295 +147,39 @@ public class IrisObject extends IrisRegistrant { } public void readLegacy(InputStream in) throws IOException { - surfaceSupportOffsets.reset(); - DataInputStream din = new DataInputStream(in); - this.w = din.readInt(); - this.h = din.readInt(); - this.d = din.readInt(); - center = new Vector3i(w / 2, h / 2, d / 2); - int s = din.readInt(); - - for (int i = 0; i < s; i++) { - IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()); - PlatformBlockState data = B.getState(din.readUTF()); - if (isStructureMarker(data)) { - continue; - } - blocks.put(pos, data); - } - - if (din.available() == 0) - return; - - try { - int size = din.readInt(); - - for (int i = 0; i < size; i++) { - states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); - } - } catch (Throwable e) { - IrisLogging.reportError(e); - } + IrisObjectIO.readLegacy(this, in); } public void read(InputStream in) throws Throwable { - surfaceSupportOffsets.reset(); - DataInputStream din = new DataInputStream(in); - this.w = din.readInt(); - this.h = din.readInt(); - this.d = din.readInt(); - if (!din.readUTF().equals("Iris V2 IOB;")) { - throw new HeaderException(); - } - center = new Vector3i(w / 2, h / 2, d / 2); - int s = din.readShort(); - int i; - KList palette = new KList<>(); - - for (i = 0; i < s; i++) { - palette.add(din.readUTF()); - } - - s = din.readInt(); - - for (i = 0; i < s; i++) { - IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()); - PlatformBlockState data = B.getState(palette.get(din.readShort())); - if (isStructureMarker(data)) { - continue; - } - blocks.put(pos, data); - } - - s = din.readInt(); - - for (i = 0; i < s; i++) { - states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); - } - } - - private static boolean isStructureMarker(PlatformBlockState data) { - if (data == null) { - return false; - } - String material = materialKey(data); - return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block") || material.equals("minecraft:structure_void"); - } - - public void write(OutputStream o) throws IOException { - DataOutputStream dos = new DataOutputStream(o); - dos.writeInt(w); - dos.writeInt(h); - dos.writeInt(d); - dos.writeUTF("Iris V2 IOB;"); - KList palette = new KList<>(); - - for (PlatformBlockState i : blocks.values()) { - palette.addIfMissing(i.key()); - } - - dos.writeShort(palette.size()); - - for (String i : palette) { - dos.writeUTF(i); - } - - dos.writeInt(blocks.size()); - - for (var entry : blocks) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - dos.writeShort(palette.indexOf(entry.getValue().key())); - } - - dos.writeInt(states.size()); - for (var entry : states) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - entry.getValue().toBinary(dos); - } - } - - public void write(OutputStream o, VolmitSender sender) throws IOException { - AtomicReference ref = new AtomicReference<>(); - CountDownLatch latch = new CountDownLatch(1); - new Job() { - private int total = blocks.size() * 3 + states.size(); - private int c = 0; - - @Override - public String getName() { - return IrisLanguage.text(RuntimeUiMessages.JOB_SAVING_OBJECT); - } - - @Override - public void execute() { - try { - DataOutputStream dos = new DataOutputStream(o); - dos.writeInt(w); - dos.writeInt(h); - dos.writeInt(d); - dos.writeUTF("Iris V2 IOB;"); - - KList palette = new KList<>(); - - for (PlatformBlockState i : blocks.values()) { - palette.addIfMissing(i.key()); - ++c; - } - total -= blocks.size() - palette.size(); - - dos.writeShort(palette.size()); - - for (String i : palette) { - dos.writeUTF(i); - ++c; - } - - dos.writeInt(blocks.size()); - - for (var entry : blocks) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - dos.writeShort(palette.indexOf(entry.getValue().key())); - ++c; - } - - dos.writeInt(states.size()); - for (var entry : states) { - var i = entry.getKey(); - dos.writeShort(i.getBlockX()); - dos.writeShort(i.getBlockY()); - dos.writeShort(i.getBlockZ()); - entry.getValue().toBinary(dos); - ++c; - } - } catch (IOException e) { - ref.set(e); - } finally { - latch.countDown(); - } - } - - @Override - public void completeWork() {} - - @Override - public int getTotalWork() { - return total; - } - - @Override - public int getWorkCompleted() { - return c; - } - }.execute(sender, true, () -> {}); - - try { - latch.await(); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted while writing object", interrupted); - } - if (ref.get() != null) - throw ref.get(); + IrisObjectIO.read(this, in); } public void read(File file) throws IOException { - try (var fin = new BufferedInputStream(new FileInputStream(file))) { - read(fin); - } catch (Throwable e) { - if (!(e instanceof HeaderException)) - IrisLogging.reportError(e); - try (var fin = new BufferedInputStream(new FileInputStream(file))) { - readLegacy(fin); - } - } + IrisObjectIO.read(this, file); + } + + public void write(OutputStream o) throws IOException { + IrisObjectIO.write(this, o); + } + + public void write(OutputStream o, VolmitSender sender) throws IOException { + IrisObjectIO.write(this, o, sender); } public void write(File file) throws IOException { - if (file == null) { - return; - } - - try (FileOutputStream out = new FileOutputStream(file)) { - write(out); - } + IrisObjectIO.write(this, file); } public void write(File file, VolmitSender sender) throws IOException { - if (file == null) { - return; - } - - try (FileOutputStream out = new FileOutputStream(file)) { - write(out, sender); - } + IrisObjectIO.write(this, file, sender); } public void shrinkwrap() { - if (blocks.isEmpty()) return; - IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); - IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - - for (IrisBlockVector i : blocks.keys()) { - min.setX(Math.min(min.getX(), i.getX())); - min.setY(Math.min(min.getY(), i.getY())); - min.setZ(Math.min(min.getZ(), i.getZ())); - max.setX(Math.max(max.getX(), i.getX())); - max.setY(Math.max(max.getY(), i.getY())); - max.setZ(Math.max(max.getZ(), i.getZ())); - } - - w = max.getBlockX() - min.getBlockX() + 1; - h = max.getBlockY() - min.getBlockY() + 1; - d = max.getBlockZ() - min.getBlockZ() + 1; - center = new Vector3i(w / 2, h / 2, d / 2); - - Vector3i offset = new Vector3i( - -center.getBlockX() - min.getBlockX(), - -center.getBlockY() - min.getBlockY(), - -center.getBlockZ() - min.getBlockZ() - ); - if (offset.getBlockX() == 0 && offset.getBlockY() == 0 && offset.getBlockZ() == 0) - return; - - VectorMap b = new VectorMap<>(); - VectorMap s = new VectorMap<>(); - IrisBlockVector shift = new IrisBlockVector(offset.getX(), offset.getY(), offset.getZ()); - - blocks.forEach((vector, data) -> { - vector.add(shift); - b.put(vector, data); - }); - - states.forEach((vector, data) -> { - vector.add(shift); - s.put(vector, data); - }); - - shrinkOffset = offset; - blocks = b; - states = s; - surfaceSupportOffsets.reset(); + IrisObjectShaping.shrinkwrap(this); } public void clean() { - VectorMap d = new VectorMap<>(); - d.putAll(blocks); - - VectorMap dx = new VectorMap<>(); - dx.putAll(states); - - blocks = d; - states = dx; - surfaceSupportOffsets.reset(); + IrisObjectShaping.clean(this); } public IrisBlockVector getSigned(int x, int y, int z) { @@ -629,7 +191,6 @@ public class IrisObject extends IrisRegistrant { } public void setUnsigned(int x, int y, int z, PlatformBlockState block) { - surfaceSupportOffsets.reset(); IrisBlockVector v = getSigned(x, y, z); if (block == null) { @@ -638,6 +199,8 @@ public class IrisObject extends IrisRegistrant { } else { blocks.put(v, block); } + + surfaceSupportOffsets.reset(); } public void setUnsignedTile(int x, int y, int z, TileData tile) { @@ -651,7 +214,6 @@ public class IrisObject extends IrisRegistrant { } public void setUnsigned(int x, int y, int z, Block block, boolean legacy) { - surfaceSupportOffsets.reset(); IrisBlockVector v = getSigned(x, y, z); if (block == null) { @@ -666,6 +228,8 @@ public class IrisObject extends IrisRegistrant { states.put(v, state); } } + + surfaceSupportOffsets.reset(); } public int place(int x, int z, IObjectPlacer placer, IrisObjectPlacement config, RNG rng, IrisData rdata) { @@ -685,833 +249,7 @@ public class IrisObject extends IrisRegistrant { } public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer listener, CarveResult c, IrisData rdata) { - IObjectPlacer placer = config.getHeightmap() != null ? new HeightmapObjectPlacer(rng, x, yv, z, config, oplacer) : oplacer; - - if (rdata != null) { - // Slope condition - if (!config.getSlopeCondition().isDefault() && - !config.getSlopeCondition().isValid(rdata.getEngine().getComplex().getSlopeStream().get(x, z)) && !config.isForcePlace()) { - return -1; - } - - // Rotation calculation - int slopeRotationY = 0; - ProceduralStream heightStream = rdata.getEngine().getComplex().getHeightStream(); - if (config.isRotateTowardsSlope()) { - // Whichever side of the rectangle that bounds the object is lowest is the 'direction' of the slope (simply said). - double hNorth = heightStream.get(x, z + ((float) d) / 2); - double hEast = heightStream.get(x + ((float) w) / 2, z); - double hSouth = heightStream.get(x, z - ((float) d) / 2); - double hWest = heightStream.get(x - ((float) w) / 2, z); - double min = Math.min(Math.min(hNorth, hEast), Math.min(hSouth, hWest)); - if (min == hNorth) { - slopeRotationY = 0; - } else if (min == hEast) { - slopeRotationY = 90; - } else if (min == hSouth) { - slopeRotationY = 180; - } else if (min == hWest) { - slopeRotationY = 270; - } - - double newRotation = config.getRotation().getYAxis().getMin() + slopeRotationY; - IrisObjectRotation originalRotation = config.getRotation(); - IrisObjectRotation slopeRotation = new IrisObjectRotation(); - slopeRotation.setXAxis(originalRotation.getXAxis()); - slopeRotation.setZAxis(originalRotation.getZAxis()); - if (newRotation == 0) { - slopeRotation.setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90)); - slopeRotation.setEnabled(originalRotation.canRotateX() || originalRotation.canRotateZ()); - } else { - slopeRotation.setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90)); - slopeRotation.setEnabled(true); - } - config = config.toPlacement(config.getPlace().toArray(new String[0])); - config.setRotation(slopeRotation); - } - } - - if (config.isSmartBore()) { - ensureSmartBored(placer.isDebugSmartBore()); - } - - boolean warped = !config.getWarp().isFlat(); - boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE; - boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT; - boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG; - boolean organic = organicFloor || ceilingHang; - boolean vacuuming = IrisObjectVacuum.isVacuumMode(config.getMode()); - boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) || - config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT || - config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic); - boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT; - KMap heightmap = config.getSnow() > 0 ? new KMap<>() : null; - int spinx = rng.imax() / 1000; - int spiny = rng.imax() / 1000; - int spinz = rng.imax() / 1000; - int rty = config.getRotation().rotate(new IrisBlockVector(0, getCenter().getBlockY(), 0), spinx, spiny, spinz).getBlockY(); - int ty = config.getTranslate().translate(new IrisBlockVector(0, getCenter().getBlockY(), 0), config.getRotation(), spinx, spiny, spinz).getBlockY(); - int y = -1; - int xx, zz; - int yrand = config.getTranslate().getYRandom(); - yrand = yrand > 0 ? rng.i(0, yrand) : yrand < 0 ? rng.i(yrand, 0) : yrand; - boolean bail = false; - - if (config.isFromBottom()) { - // todo Convert this to a dedicated mode. - y = (getH() + 1) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { - bail = true; - } - } - } else if (yv < 0) { - if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT - || organic || vacuuming) { - y = (c != null ? c.getSurface() : placer.getHighest(x, z, getLoader(), config.isUnderwater())) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { - bail = true; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.STILT)) { - IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - for (int i = minX; i <= maxX; i++) { - for (int ii = minZ; ii <= maxZ; ii++) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { - bail = true; - break; - } - } - if (h > y) - y = h; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.FAST_MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT)) { - IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - - int xRadius = (rotatedDimensions.getBlockX() / 2); - int xLength = xRadius + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zRadius = (rotatedDimensions.getBlockZ() / 2); - int zLength = zRadius + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - - for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { - for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { - bail = true; - break; - } - } - if (h > y) - y = h; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.MIN_STILT) { - y = rdata.getEngine().getHeight() + 1; - IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - - int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - for (int i = minX; i <= maxX; i++) { - for (int ii = minZ; ii <= maxZ; ii++) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { - bail = true; - break; - } - } - if (h < y) { - y = h; - } - } - } - } else if (config.getMode().equals(ObjectPlaceMode.FAST_MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT) { - y = rdata.getEngine().getHeight() + 1; - IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - - int xRadius = (rotatedDimensions.getBlockX() / 2); - int xLength = xRadius + offset.getBlockX(); - int minX = Math.min(x - xLength, x + xLength); - int maxX = Math.max(x - xLength, x + xLength); - int zRadius = (rotatedDimensions.getBlockZ() / 2); - int zLength = zRadius + offset.getBlockZ(); - int minZ = Math.min(z - zLength, z + zLength); - int maxZ = Math.max(z - zLength, z + zLength); - - for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { - for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { - int h = placer.getHighest(i, ii, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { - bail = true; - break; - } - } - if (h < y) { - y = h; - } - } - } - } else if (config.getMode().equals(ObjectPlaceMode.PAINT)) { - y = placer.getHighest(x, z, getLoader(), config.isUnderwater()) + rty; - if (!config.isForcePlace()) { - if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { - bail = true; - } - } - } else if (config.getMode().equals(ObjectPlaceMode.FLOATING)) { - y = rty; - } - } else { - y = yv; - if (!config.isForcePlace() && !rawStructurePiece) { - if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { - bail = true; - } - } - } - - if (yv >= 0 && config.isBottom() && !rawStructurePiece) { - y += Math.floorDiv(h, 2); - CarvingMode carvingMode = config.getCarvingSupport(); - if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) { - if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { - bail = true; - } - } - } - - if (yv < 0 - && !config.isForcePlace() - && !config.isFromBottom() - && config.getMode() != ObjectPlaceMode.FLOATING - && !rawStructurePiece - && config.getCarvingSupport().supportsSurface() - && placer.getEngine() != null - && placer.getEngine().getDimension().isBedrock() - && y <= 1) { - warnImplausibleBedrockPlacement(placer, config, x, y, z); - return -1; - } - - if (bail && !config.isForcePlace()) { - return -1; - } - - // Surface-anchored placements may never roof or bridge a carved hole. Explicit-Y anchors are only - // guarded for SURFACE_ONLY: ANYWHERE covers the inverted upper dimension and CARVING_ONLY covers - // cave anchors, and neither reads the terrain surface this stencil samples. - boolean surfaceAnchored = yv < 0 - ? config.getCarvingSupport().supportsSurface() - : config.getCarvingSupport() == CarvingMode.SURFACE_ONLY; - if (surfaceAnchored - && !config.isForcePlace() - && !config.isFromBottom() - && config.getMode() != ObjectPlaceMode.FLOATING - && !rawStructurePiece - && !config.isUnderwater() - && !config.isOnwater() - && config.isRequireSurfaceSupport() - && IrisSurfaceSupport.isUnsupported(oplacer, getLoader(), x, z, config.getTranslate(), - config.getRotation(), spinx, spiny, spinz, getSurfaceSupportOffsets(), - config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth())) { - return -1; - } - - if (yv < 0 && !config.getMode().equals(ObjectPlaceMode.FLOATING) && !rawStructurePiece) { - if (!config.isForcePlace() && !config.isUnderwater() && !config.isOnwater() && placer.isUnderwater(x, z)) { - return -1; - } - } - - if (!config.isForcePlace() && !rawStructurePiece && c != null && Math.max(0, h + yrand + ty) + 1 >= c.getHeight()) { - return -1; - } - - if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { - return -1; - } - - if (!config.isForcePlace() && !rawStructurePiece && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) { - return -1; - } - - if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { - Engine engine = rdata.getEngine(); - IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - for (int i = x - Math.floorDiv(w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(w, 2) - (w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { - for (int j = y - Math.floorDiv(h, 2) + (int) offset.getY(); j <= y + Math.floorDiv(h, 2) - (h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { - for (int k = z - Math.floorDiv(d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(d, 2) - (d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) { - PlacedObject p = engine.getObjectPlacement(i, j, k); - if (p == null) continue; - IrisObject o = p.getObject(); - if (o == null) continue; - String key = o.getLoadKey(); - if (key != null) { - if (config.getForbiddenCollisions().contains(key) && !config.getAllowedCollisions().contains(key)) { - return -1; - } - } - } - } - } - } - - if (config.isBore()) { - IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); - for (int i = x - Math.floorDiv(w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(w, 2) - (w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { - for (int j = y - Math.floorDiv(h, 2) - config.getBoreExtendMinY() + (int) offset.getY(); j <= y + Math.floorDiv(h, 2) + config.getBoreExtendMaxY() - (h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { - for (int k = z - Math.floorDiv(d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(d, 2) - (d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) { - placer.set(i, j, k, States.AIR); - } - } - } - } - - int lowest = Integer.MAX_VALUE; - int topLayer = Integer.MIN_VALUE; - int vacuumLowest = Integer.MAX_VALUE; - int vacuumHighest = Integer.MIN_VALUE; - y += yrand; - readLock.lock(); - - KMap markers = null; - - try { - if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) { - markers = new KMap<>(); - var list = StreamSupport.stream(blocks.keys().spliterator(), false) - .collect(KList.collector()); - - for (IrisObjectMarker j : config.getMarkers()) { - IrisMarker marker = getLoader().getMarkerLoader().load(j.getMarker()); - - if (marker == null) { - continue; - } - - int max = j.getMaximumMarkers(); - for (IrisBlockVector i : list.shuffle()) { - if (max <= 0) { - break; - } - - PlatformBlockState data = blocks.get(i); - if (data == null) { - continue; - } - - for (PlatformBlockState k : j.getMark(rdata)) { - if (max <= 0) { - break; - } - - if (j.isExact() ? k.matches(data) : materialKey(k).equals(materialKey(data))) { - boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0))); - boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0))); - - if (!marker.isEmptyAbove() || (a && fff)) { - markers.put(i, j.getMarker()); - max--; - } - } - } - } - } - } - - for (var entry : blocks) { - var g = entry.getKey(); - PlatformBlockState d; - TileData tile = null; - - try { - d = entry.getValue(); - tile = states.get(g); - } catch (Throwable e) { - IrisLogging.reportError(e); - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (cme)"); - d = States.AIR; - } - - if (d == null) { - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (null)"); - d = States.AIR; - } - - IrisBlockVector i = g.clone(); - PlatformBlockState data = d; - i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); - if (ceilingHang) { - i.setY(-i.getBlockY()); - } - i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - - if (stilting && shouldStilt(data)) { - if (i.getBlockY() < lowest) { - lowest = i.getBlockY(); - } - if (i.getBlockY() > topLayer) { - topLayer = i.getBlockY(); - } - } - - if (placer.isPreventingDecay() && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) { - data = data.withProperty("persistent", "true"); - } - - for (IrisObjectReplace j : config.getEdit()) { - if (rng.chance(j.getChance())) { - for (PlatformBlockState k : j.getFind(rdata)) { - if (j.isExact() ? k.matches(data) : materialKey(k).equals(materialKey(data))) { - PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); - - if (materialKey(newData).equals(materialKey(data)) && !(newData.isCustom() || data.isCustom())) - data = BlockDataMergeSupport.merge(data, newData); - else - data = newData; - - Optional t = j.getReplace().getTile(rng, x, y, z, rdata); - if (t.isPresent()) { - tile = t.get(); - } - } - } - } - } - - data = config.getRotation().rotate(data, spinx, spiny, spinz); - xx = x + (int) Math.round(i.getX()); - - int yy = y + (int) Math.round(i.getY()); - zz = z + (int) Math.round(i.getZ()); - - if (warped) { - xx += config.warp(rng, i.getX() + x, i.getY() + y, i.getZ() + z, getLoader()); - zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, getLoader()); - } - - if (yv < 0 && (config.getMode().equals(ObjectPlaceMode.PAINT)) && !B.isVineBlock(data)) { - yy = (int) Math.round(i.getY()) + Math.floorDiv(h, 2) + placer.getHighest(xx, zz, getLoader(), config.isUnderwater()); - } - - if (heightmap != null) { - Position2 pos = new Position2(xx, zz); - - if (!heightmap.containsKey(pos)) { - heightmap.put(pos, yy); - } - - if (heightmap.get(pos) < yy) { - heightmap.put(pos, yy); - } - } - - if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { - continue; - } - - if (IrisProceduralBlocks.hasProperty(data, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) { - data = data.withProperty("waterlogged", "true"); - } - - if (B.isVineBlock(data)) { - data = attachVineFaces(placer, data, xx, yy, zz); - } - - PlatformBlockState existingState = placer.get(xx, yy, zz); - boolean wouldReplace = B.isSolid(existingState) && B.isVineBlock(data); - String material = materialKey(data); - boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air"); - boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace); - - if (data.isCustom() || place) { - placer.set(xx, yy, zz, data); - if (tile != null) { - placer.setTile(xx, yy, zz, tile); - } - if (markers != null && markers.containsKey(g)) { - placer.setData(xx, yy, zz, new MatterMarker(markers.get(g))); - } - if (listener != null) { - listener.accept(new BlockPosition(xx, yy, zz), data); - } - if (vacuuming && yy < vacuumLowest) { - vacuumLowest = yy; - } - if (vacuuming && yy > vacuumHighest) { - vacuumHighest = yy; - } - } - } - } catch (Throwable e) { - e.printStackTrace(); - IrisLogging.reportError(e); - } - readLock.unlock(); - - if (stilting) { - readLock.lock(); - IrisStiltSettings settings = config.getStiltSettings(); - - double erodeCentroidX = 0; - double erodeCentroidZ = 0; - double erodeMaxDist = 1; - if (eroding) { - int centroidCount = 0; - for (IrisBlockVector g : blocks.keys()) { - IrisBlockVector rot = config.getRotation().rotate(g.clone(), spinx, spiny, spinz).clone(); - rot = config.getTranslate().translate(rot.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - if (rot.getBlockY() == lowest) { - PlatformBlockState bd = blocks.get(g); - if (bd != null && shouldStilt(bd)) { - erodeCentroidX += rot.getX(); - erodeCentroidZ += rot.getZ(); - centroidCount++; - } - } - } - if (centroidCount > 0) { - erodeCentroidX /= centroidCount; - erodeCentroidZ /= centroidCount; - } - for (IrisBlockVector g : blocks.keys()) { - IrisBlockVector rot = config.getRotation().rotate(g.clone(), spinx, spiny, spinz).clone(); - rot = config.getTranslate().translate(rot.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - if (rot.getBlockY() == lowest) { - PlatformBlockState bd = blocks.get(g); - if (bd != null && shouldStilt(bd)) { - double dx = rot.getX() - erodeCentroidX; - double dz = rot.getZ() - erodeCentroidZ; - double dist = Math.sqrt(dx * dx + dz * dz); - if (dist > erodeMaxDist) { - erodeMaxDist = dist; - } - } - } - } - } - - for (IrisBlockVector g : blocks.keys()) { - PlatformBlockState sourceData; - try { - sourceData = blocks.get(g); - } catch (Throwable e) { - IrisLogging.reportError(e); - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (stilt cme)"); - sourceData = States.AIR; - } - - if (sourceData == null) { - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + getLoadKey() + " (stilt null)"); - sourceData = States.AIR; - } - - if (!shouldStilt(sourceData)) { - continue; - } - - PlatformBlockState d = sourceData; - if (settings != null && settings.getPalette() != null) { - d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); - } else { - String mat = materialKey(d); - if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) { - d = B.getState("minecraft:dirt"); - } - } - - IrisBlockVector i = g.clone(); - i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); - if (ceilingHang) { - i.setY(-i.getBlockY()); - } - i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - d = config.getRotation().rotate(d, spinx, spiny, spinz); - - int targetLayer = ceilingHang ? topLayer : lowest; - if (i.getBlockY() != targetLayer) - continue; - - for (IrisObjectReplace j : config.getEdit()) { - if (rng.chance(j.getChance())) { - for (PlatformBlockState k : j.getFind(rdata)) { - if (d == null) { - continue; - } - if (j.isExact() ? k.matches(d) : materialKey(k).equals(materialKey(d))) { - PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); - - if (materialKey(newData).equals(materialKey(d))) { - d = BlockDataMergeSupport.merge(d, newData); - } else { - d = newData; - } - } - } - } - } - - if (d == null || !d.isOccluding()) - continue; - - xx = x + (int) Math.round(i.getX()); - zz = z + (int) Math.round(i.getZ()); - - if (warped) { - xx += config.warp(rng, i.getX() + x, i.getY() + y, i.getZ() + z, getLoader()); - zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, getLoader()); - } - - if (organic) { - int startY = targetLayer + y; - int maxScan = settings != null ? Math.max(1, settings.getOrganicMaxScan()) : 48; - int jitterMax = settings != null ? Math.max(0, settings.getOrganicJitter()) : 3; - double scratch = settings != null ? Math.max(0, Math.min(1, settings.getOrganicScratch())) : 0.55; - long colHash = ((long) xx * 341873128712L) ^ ((long) zz * 132897987541L); - int jitter = jitterMax > 0 ? (int) (Math.abs(colHash) % (jitterMax + 1)) : 0; - - if (ceilingHang) { - int scan = 0; - int solidY = startY + 1; - while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { - solidY++; - scan++; - } - int topBound = (scan < maxScan ? solidY - 1 : startY + Math.min(maxScan, 8)) - jitter; - int total = topBound - startY; - for (int j = startY; j <= topBound; j++) { - if (scratch > 0 && total > 0) { - double ratio = (double) (j - startY) / total; - if (ratio > (1.0 - scratch)) { - long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); - double skipChance = (ratio - (1.0 - scratch)) / scratch; - if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { - continue; - } - } - } - placer.set(xx, j, zz, d); - } - } else { - int scan = 0; - int solidY = startY - 1; - while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { - solidY--; - scan++; - } - int bottomBound = (scan < maxScan ? solidY + 1 : startY - Math.min(maxScan, 8)) + jitter; - int total = startY - bottomBound; - for (int j = startY; j >= bottomBound; j--) { - if (scratch > 0 && total > 0) { - double ratio = (double) (startY - j) / total; - if (ratio > (1.0 - scratch)) { - long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); - double skipChance = (ratio - (1.0 - scratch)) / scratch; - if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { - continue; - } - } - } - placer.set(xx, j, zz, d); - } - } - continue; - } - - int highest = placer.getHighest(xx, zz, getLoader(), true); - - if (IrisProceduralBlocks.hasProperty(d, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) { - d = d.withProperty("waterlogged", "true"); - } - - int lowerBound = highest - 1; - if (settings != null) { - lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); - if (settings.getYMax() != 0) - lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); - } - - if (eroding) { - double dx = i.getX() - erodeCentroidX; - double dz = i.getZ() - erodeCentroidZ; - double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; - normalizedDist = Math.min(normalizedDist, 1.0); - int totalDepth = (lowest + y) - lowerBound; - int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5)); - lowerBound = (lowest + y) - erodeDepth; - } - - for (int j = lowest + y; j > lowerBound; j--) { - PlatformBlockState fluidState = placer.get(xx, j, zz); - if (B.isFluid(fluidState)) { - break; - } - if (eroding) { - int depth = (lowest + y) - j; - int totalDepth = (lowest + y) - lowerBound; - double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0; - if (depthRatio > 0.4) { - long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L)); - double skipChance = (depthRatio - 0.4) / 0.6; - if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) { - continue; - } - } - } - - if (B.isVineBlock(d)) { - d = attachVineFaces(placer, d, xx, j, zz); - } - placer.set(xx, j, zz, d); - } - - } - - readLock.unlock(); - } - - if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) { - IrisBlockVector rotDim = config.getRotation().rotate(new IrisBlockVector(getW(), getH(), getD()), spinx, spiny, spinz).clone(); - int lowX = IrisObjectVacuum.footprintLow(rotDim.getBlockX()); - int highX = IrisObjectVacuum.footprintHigh(rotDim.getBlockX()); - int lowZ = IrisObjectVacuum.footprintLow(rotDim.getBlockZ()); - int highZ = IrisObjectVacuum.footprintHigh(rotDim.getBlockZ()); - int centerX = x + config.getTranslate().getX(); - int centerZ = z + config.getTranslate().getZ(); - vacuumTerrain(placer, config, centerX, centerZ, lowX, highX, lowZ, highZ, vacuumLowest, vacuumHighest); - } - - if (heightmap != null) { - RNG rngx = rng.nextParallelRNG(3468854); - - for (Position2 i : heightmap.k()) { - int vx = i.getX(); - int vy = heightmap.get(i); - int vz = i.getZ(); - - if (config.getSnow() > 0) { - int height = rngx.i(0, (int) (config.getSnow() * 7)); - placer.set(vx, vy + 1, vz, States.SNOW_LAYERS[Math.max(Math.min(height, 7), 0)]); - } - } - } - - return y; - } - - static boolean shouldPlaceObjectBlock(boolean rawStructurePiece, boolean air, boolean wouldReplace) { - return !wouldReplace && (rawStructurePiece || !air); - } - - private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) { - String key = getLoadKey(); - String fingerprint = (key == null ? "" : key) + "|" + config.getMode(); - long now = System.currentTimeMillis(); - Long last = IMPLAUSIBLE_BEDROCK_WARNS.get(fingerprint); - if (last != null && now - last < IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS) { - return; - } - IMPLAUSIBLE_BEDROCK_WARNS.put(fingerprint, now); - IrisLogging.warn("Implausible object placement rejected: " - + (key == null ? "" : key) - + " resolved anchorY=" + y + " at (" + x + "," + z + ") mode=" + config.getMode() - + " carving=" + config.getCarvingSupport() - + ". Surface-anchored placement should never land on the bedrock row. " - + "Height sampling returned a bogus value — not configured for floor placement " - + "(forcePlace=false, fromBottom=false, mode!=FLOATING). Skipping to protect bedrock."); - } - - private void vacuumTerrain(IObjectPlacer placer, IrisObjectPlacement config, int centerX, int centerZ, int lowX, int highX, int lowZ, int highZ, int baseY, int topY) { - ObjectPlaceMode mode = config.getMode(); - IrisVacuumSettings settings = config.getVacuumSettings(); - int radius = IrisObjectVacuum.resolveRadius(mode, settings); - int step = IrisObjectVacuum.resolveStep(mode); - double falloff = IrisObjectVacuum.resolveFalloff(settings); - int jitter = settings != null ? Math.max(0, settings.getOrganicJitter()) : 4; - boolean organicEdge = mode == ObjectPlaceMode.VACUUM_ORGANIC; - boolean wavyEdge = mode == ObjectPlaceMode.VACUUM_WAVY; - double waveAmplitude = IrisObjectVacuum.resolveWaveAmplitude(settings); - double waveScale = IrisObjectVacuum.resolveWaveScale(settings); - SimplexNoise waveNoise = (wavyEdge && waveAmplitude > 0) ? new SimplexNoise(VACUUM_WAVE_SEED) : null; - int meetY = baseY - 1; - - IrisComplex complex = placer.getEngine().getComplex(); - int worldMin = placer.getEngine().getMinHeight(); - int worldMax = worldMin + placer.getEngine().getHeight() - 1; - - for (int dx = lowX - radius; dx <= highX + radius; dx += step) { - for (int dz = lowZ - radius; dz <= highZ + radius; dz += step) { - int cx = centerX + dx; - int cz = centerZ + dz; - double effRadius = radius; - if (organicEdge && jitter > 0) { - long h = ((long) cx * 341873128712L) ^ ((long) cz * 132897987541L); - double n = ((Math.abs(h) % 1000) / 1000.0) - 0.5; - effRadius = Math.max(1.0, radius + (n * 2.0 * jitter)); - } - int origY = placer.getHighest(cx, cz, getLoader(), true); - int targetY = IrisObjectVacuum.columnTargetY(dx, dz, lowX, highX, lowZ, highZ, effRadius, falloff, origY, meetY); - if (waveNoise != null) { - int outX = IrisObjectVacuum.outset(dx, lowX, highX); - int outZ = IrisObjectVacuum.outset(dz, lowZ, highZ); - double waveDistance = Math.sqrt((double) (outX * outX) + (double) (outZ * outZ)); - double sample = waveNoise.noiseSigned(cx * waveScale, cz * waveScale); - targetY += IrisObjectVacuum.waveOffset(waveDistance, effRadius, sample, waveAmplitude); - } - if (targetY == origY) { - continue; - } - targetY = Math.max(worldMin + 1, Math.min(worldMax, targetY)); - if (targetY > origY) { - PlatformBlockState fill = complex != null ? complex.getRockStream().get(cx, cz) : null; - if (B.isAir(fill)) { - fill = States.STONE; - } - for (int yy = origY + 1; yy <= targetY; yy++) { - placer.set(cx, yy, cz, fill); - } - } else if (targetY < origY) { - boolean inside = IrisObjectVacuum.outset(dx, lowX, highX) == 0 && IrisObjectVacuum.outset(dz, lowZ, highZ) == 0; - int carveFloor = IrisObjectVacuum.carveFloorY(targetY, topY, inside); - for (int yy = origY; yy >= carveFloor; yy--) { - placer.set(cx, yy, cz, States.AIR); - } - } - } - } - } - - private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) { - CarvingMode carvingMode = placement.getCarvingSupport(); - return switch (carvingMode) { - case SURFACE_ONLY -> placer.isCarved(x, y, z); - case CARVING_ONLY -> !isCarvedCaveAnchor(placer, x, y, z); - case ANYWHERE -> false; - }; + return new IrisObjectPlacementRunner(this).place(x, yv, z, oplacer, config, rng, listener, c, rdata); } KList getSurfaceSupportOffsets() { @@ -1542,297 +280,58 @@ public class IrisObject extends IrisRegistrant { }); } - private boolean isCarvedCaveAnchor(IObjectPlacer placer, int x, int y, int z) { - return placer.isCarved(x, y, z) - || placer.isCarved(x, y - 1, z) - || placer.isCarved(x, y - 2, z) - || placer.isCarved(x, y - 3, z); - } - - private boolean shouldAutoWaterlogBlock(IObjectPlacer placer, IrisObjectPlacement placement, int yv, int x, int y, int z) { - if (!(placement.isWaterloggable() || placement.isUnderwater())) { - return false; - } - - if (yv >= 0 && placement.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { - return false; - } - - PlatformBlockState existing = placer.get(x, y, z); - if (existing == null) { - return false; - } - - return B.isWater(existing) || B.isWaterLogged(existing); - } - - private static PlatformBlockState attachVineFaces(IObjectPlacer placer, PlatformBlockState data, int x, int y, int z) { - PlatformBlockState result = data; - for (String face : IrisProceduralBlocks.FACE_PROPERTIES) { - if (!IrisProceduralBlocks.hasProperty(data, face)) { - continue; - } - int[] mod = IrisProceduralBlocks.faceOffset(face); - PlatformBlockState facing = placer.get(x + mod[0], y + mod[1], z + mod[2]); - if (B.isSolid(facing) && !B.isVineBlock(facing)) { - result = result.withProperty(face, "true"); - } - } - return result; - } - public IrisObject rotateCopy(IrisObjectRotation rt) { - IrisObject copy = copy(); - copy.rotate(rt, 0, 0, 0); - return copy; + return IrisObjectTransforms.rotateCopy(this, rt); } - public void rotate(IrisObjectRotation r, int spinx, int spiny, int spinz) { - writeLock.lock(); - VectorMap d = new VectorMap<>(); - - for (var entry : blocks) { - d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz)); - } - - VectorMap dx = new VectorMap<>(); - - for (var entry : states) { - dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue()); - } - - blocks = d; - states = dx; - surfaceSupportOffsets.reset(); - shrinkwrap(); - writeLock.unlock(); + public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) { + return IrisObjectTransforms.scaled(this, scale, interpolation); } public void place(Location at) { readLock.lock(); - for (var entry : blocks) { - var i = entry.getKey(); - Block b = at.clone().add(0, getCenter().getY(), 0).add(i.getX(), i.getY(), i.getZ()).getBlock(); - b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false); + try { + for (var entry : blocks) { + var i = entry.getKey(); + Block b = at.clone().add(0, getCenter().getY(), 0).add(i.getX(), i.getY(), i.getZ()).getBlock(); + b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false); - if (states.containsKey(i)) { - IrisLogging.info(Objects.requireNonNull(states.get(i)).toString()); - Objects.requireNonNull(states.get(i)).toBukkitTry(b); + if (states.containsKey(i)) { + IrisLogging.info(Objects.requireNonNull(states.get(i)).toString()); + Objects.requireNonNull(states.get(i)).toBukkitTry(b); + } } + } finally { + readLock.unlock(); } - readLock.unlock(); } public void placeCenterY(Location at) { readLock.lock(); - for (var entry : blocks) { - var i = entry.getKey(); - Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock(); - b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false); + try { + for (var entry : blocks) { + var i = entry.getKey(); + Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock(); + b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false); - if (states.containsKey(i)) { - Objects.requireNonNull(states.get(i)).toBukkitTry(b); + if (states.containsKey(i)) { + Objects.requireNonNull(states.get(i)).toBukkitTry(b); + } } + } finally { + readLock.unlock(); } - readLock.unlock(); } public void unplaceCenterY(Location at) { readLock.lock(); - for (IrisBlockVector i : blocks.keys()) { - at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock().setBlockData((BlockData) States.AIR.nativeHandle(), false); - } - readLock.unlock(); - } - - public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) { - if (interpolation == null) { - interpolation = IrisObjectPlacementScaleInterpolator.NONE; - } - IrisVector sm1 = new IrisVector(scale - 1, scale - 1, scale - 1); - scale = Math.max(0.001, Math.min(50, scale)); - if (scale < 1) { - scale = scale - 0.0001; - } - - IrisPosition l1 = getAABB().max(); - IrisPosition l2 = getAABB().min(); - VectorMap placeBlock = new VectorMap<>(); - - IrisVector center = new IrisVector(getCenter().getX(), getCenter().getY(), getCenter().getZ()); - if (getH() == 2) { - center = center.setY(center.getBlockY() + 0.5); - } - if (getW() == 2) { - center = center.setX(center.getBlockX() + 0.5); - } - if (getD() == 2) { - center = center.setZ(center.getBlockZ() + 0.5); - } - - IrisObject oo = new IrisObject((int) Math.ceil((w * scale) + (scale * 2)), (int) Math.ceil((h * scale) + (scale * 2)), (int) Math.ceil((d * scale) + (scale * 2))); - oo.setLoadKey(getLoadKey()); - oo.setLoader(getLoader()); - oo.setLoadFile(getLoadFile()); - - readLock.lock(); - for (var entry : blocks) { - PlatformBlockState bd = entry.getValue(); - placeBlock.put(entry.getKey().clone().add(HALF).subtract(center) - .multiply(scale).add(sm1).toBlockVector(), bd); - } - readLock.unlock(); - - for (var entry : placeBlock) { - IrisBlockVector v = entry.getKey(); - if (scale > 1) { - for (IrisBlockVector vec : blocksBetweenTwoPoints(v.clone().add(center), v.clone().add(center).add(sm1))) { - oo.blocks.put(vec, entry.getValue()); - } - } else { - oo.setUnsigned(v.getBlockX(), v.getBlockY(), v.getBlockZ(), entry.getValue()); + try { + for (IrisBlockVector i : blocks.keys()) { + at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock().setBlockData((BlockData) States.AIR.nativeHandle(), false); } + } finally { + readLock.unlock(); } - - if (scale > 1) { - switch (interpolation) { - case TRILINEAR -> oo.trilinear((int) Math.round(scale)); - case TRICUBIC -> oo.tricubic((int) Math.round(scale)); - case TRIHERMITE -> oo.trihermite((int) Math.round(scale)); - } - } - - return oo; - } - - public void trilinear(int rad) { - writeLock.lock(); - VectorMap v = blocks; - VectorMap b = new VectorMap<>(); - IrisPosition min = getAABB().min(); - IrisPosition max = getAABB().max(); - - for (int x = min.getX(); x <= max.getX(); x++) { - for (int y = min.getY(); y <= max.getY(); y++) { - for (int z = min.getZ(); z <= max.getZ(); z++) { - if (IrisInterpolation.getTrilinear(x, y, z, rad, (xx, yy, zz) -> { - PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); - - if (B.isAir(data)) { - return 0; - } - - return 1; - }) >= 0.5) { - b.put(new IrisBlockVector(x, y, z), nearestBlockData(x, y, z)); - } else { - b.put(new IrisBlockVector(x, y, z), States.AIR); - } - } - } - } - - blocks = b; - surfaceSupportOffsets.reset(); - writeLock.unlock(); - } - - public void tricubic(int rad) { - writeLock.lock(); - VectorMap v = blocks; - VectorMap b = new VectorMap<>(); - IrisPosition min = getAABB().min(); - IrisPosition max = getAABB().max(); - - for (int x = min.getX(); x <= max.getX(); x++) { - for (int y = min.getY(); y <= max.getY(); y++) { - for (int z = min.getZ(); z <= max.getZ(); z++) { - if (IrisInterpolation.getTricubic(x, y, z, rad, (xx, yy, zz) -> { - PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); - - if (B.isAir(data)) { - return 0; - } - - return 1; - }) >= 0.5) { - b.put(new IrisBlockVector(x, y, z), nearestBlockData(x, y, z)); - } else { - b.put(new IrisBlockVector(x, y, z), States.AIR); - } - } - } - } - - blocks = b; - surfaceSupportOffsets.reset(); - writeLock.unlock(); - } - - public void trihermite(int rad) { - trihermite(rad, 0D, 0D); - } - - public void trihermite(int rad, double tension, double bias) { - writeLock.lock(); - VectorMap v = blocks; - VectorMap b = new VectorMap<>(); - IrisPosition min = getAABB().min(); - IrisPosition max = getAABB().max(); - - for (int x = min.getX(); x <= max.getX(); x++) { - for (int y = min.getY(); y <= max.getY(); y++) { - for (int z = min.getZ(); z <= max.getZ(); z++) { - if (IrisInterpolation.getTrihermite(x, y, z, rad, (xx, yy, zz) -> { - PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); - - if (B.isAir(data)) { - return 0; - } - - return 1; - }, tension, bias) >= 0.5) { - b.put(new IrisBlockVector(x, y, z), nearestBlockData(x, y, z)); - } else { - b.put(new IrisBlockVector(x, y, z), States.AIR); - } - } - } - } - - blocks = b; - surfaceSupportOffsets.reset(); - writeLock.unlock(); - } - - private PlatformBlockState nearestBlockData(int x, int y, int z) { - IrisBlockVector vv = new IrisBlockVector(x, y, z); - readLock.lock(); - PlatformBlockState r = blocks.get(vv); - - if (!B.isAir(r)) { - return r; - } - - double d = Double.MAX_VALUE; - - for (var entry : blocks) { - PlatformBlockState dat = entry.getValue(); - - if (B.isAir(dat)) { - continue; - } - - double dx = entry.getKey().distanceSquared(vv); - - if (dx < d) { - d = dx; - r = dat; - } - } - readLock.unlock(); - - return r; } public int volume() { @@ -1852,10 +351,4 @@ public class IrisObject extends IrisRegistrant { @Override public void scanForErrors(JSONObject p, VolmitSender sender) { } - - private static class HeaderException extends IOException { - public HeaderException() { - super("Invalid Header"); - } - } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java new file mode 100644 index 000000000..ca2315b0a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectIO.java @@ -0,0 +1,296 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeUiMessages; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.jobs.Job; +import art.arcane.volmlib.util.collection.KList; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Binary (.iob) persistence for {@link IrisObject}. The field layout written here is pinned by the on-disk + * format - do not reorder reads or writes. + */ +final class IrisObjectIO { + private IrisObjectIO() { + } + + static IrisBlockVector sampleSize(File file) throws IOException { + try (DataInputStream din = new DataInputStream(new FileInputStream(file))) { + return new IrisBlockVector(din.readInt(), din.readInt(), din.readInt()); + } + } + + static void readLegacy(IrisObject self, InputStream in) throws IOException { + self.surfaceSupportOffsets.reset(); + DataInputStream din = new DataInputStream(in); + self.w = din.readInt(); + self.h = din.readInt(); + self.d = din.readInt(); + self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2); + int s = din.readInt(); + + for (int i = 0; i < s; i++) { + IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()); + PlatformBlockState data = B.getState(din.readUTF()); + if (isStructureMarker(data)) { + continue; + } + self.blocks.put(pos, data); + } + + if (din.available() == 0) + return; + + try { + int size = din.readInt(); + + for (int i = 0; i < size; i++) { + self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + static void read(IrisObject self, InputStream in) throws Throwable { + self.surfaceSupportOffsets.reset(); + DataInputStream din = new DataInputStream(in); + self.w = din.readInt(); + self.h = din.readInt(); + self.d = din.readInt(); + if (!din.readUTF().equals("Iris V2 IOB;")) { + throw new HeaderException(); + } + self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2); + int s = din.readShort(); + int i; + KList palette = new KList<>(); + + for (i = 0; i < s; i++) { + palette.add(din.readUTF()); + } + + s = din.readInt(); + + for (i = 0; i < s; i++) { + IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()); + PlatformBlockState data = B.getState(palette.get(din.readShort())); + if (isStructureMarker(data)) { + continue; + } + self.blocks.put(pos, data); + } + + s = din.readInt(); + + for (i = 0; i < s; i++) { + self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din)); + } + } + + static void read(IrisObject self, File file) throws IOException { + try (var fin = new BufferedInputStream(new FileInputStream(file))) { + read(self, fin); + } catch (Throwable e) { + if (!(e instanceof HeaderException)) + IrisLogging.reportError(e); + try (var fin = new BufferedInputStream(new FileInputStream(file))) { + readLegacy(self, fin); + } + } + } + + private static boolean isStructureMarker(PlatformBlockState data) { + if (data == null) { + return false; + } + String material = IrisObjectShaping.materialKey(data); + return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block") || material.equals("minecraft:structure_void"); + } + + static void write(IrisObject self, OutputStream o) throws IOException { + DataOutputStream dos = new DataOutputStream(o); + dos.writeInt(self.w); + dos.writeInt(self.h); + dos.writeInt(self.d); + dos.writeUTF("Iris V2 IOB;"); + KList palette = new KList<>(); + + for (PlatformBlockState i : self.blocks.values()) { + palette.addIfMissing(i.key()); + } + + dos.writeShort(palette.size()); + + for (String i : palette) { + dos.writeUTF(i); + } + + dos.writeInt(self.blocks.size()); + + for (var entry : self.blocks) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + dos.writeShort(palette.indexOf(entry.getValue().key())); + } + + dos.writeInt(self.states.size()); + for (var entry : self.states) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + entry.getValue().toBinary(dos); + } + } + + static void write(IrisObject self, OutputStream o, VolmitSender sender) throws IOException { + AtomicReference ref = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + new Job() { + private int total = self.blocks.size() * 3 + self.states.size(); + private int c = 0; + + @Override + public String getName() { + return IrisLanguage.text(RuntimeUiMessages.JOB_SAVING_OBJECT); + } + + @Override + public void execute() { + try { + DataOutputStream dos = new DataOutputStream(o); + dos.writeInt(self.w); + dos.writeInt(self.h); + dos.writeInt(self.d); + dos.writeUTF("Iris V2 IOB;"); + + KList palette = new KList<>(); + + for (PlatformBlockState i : self.blocks.values()) { + palette.addIfMissing(i.key()); + ++c; + } + total -= self.blocks.size() - palette.size(); + + dos.writeShort(palette.size()); + + for (String i : palette) { + dos.writeUTF(i); + ++c; + } + + dos.writeInt(self.blocks.size()); + + for (var entry : self.blocks) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + dos.writeShort(palette.indexOf(entry.getValue().key())); + ++c; + } + + dos.writeInt(self.states.size()); + for (var entry : self.states) { + var i = entry.getKey(); + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + entry.getValue().toBinary(dos); + ++c; + } + } catch (IOException e) { + ref.set(e); + } finally { + latch.countDown(); + } + } + + @Override + public void completeWork() {} + + @Override + public int getTotalWork() { + return total; + } + + @Override + public int getWorkCompleted() { + return c; + } + }.execute(sender, true, () -> {}); + + try { + latch.await(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while writing object", interrupted); + } + if (ref.get() != null) + throw ref.get(); + } + + static void write(IrisObject self, File file) throws IOException { + if (file == null) { + return; + } + + try (FileOutputStream out = new FileOutputStream(file)) { + write(self, out); + } + } + + static void write(IrisObject self, File file, VolmitSender sender) throws IOException { + if (file == null) { + return; + } + + try (FileOutputStream out = new FileOutputStream(file)) { + write(self, out, sender); + } + } + + private static class HeaderException extends IOException { + public HeaderException() { + super("Invalid Header"); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java index a2d0da86d..35aca3ac3 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectLimit.java @@ -37,12 +37,12 @@ public class IrisObjectLimit { @MinNumber(0) @MaxNumber(1024) @Desc("The minimum height for placement (bottom of object)") - private int minimumHeight = -2048; //TODO: WARNING HEIGHT + private int minimumHeight = -2048; @MinNumber(0) @MaxNumber(1024) @Desc("The maximum height for placement (top of object)") - private int maximumHeight = 2048; //TODO: WARNING HEIGHT + private int maximumHeight = 2048; public boolean canPlace(int h, int l) { return h <= maximumHeight && l >= minimumHeight; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java new file mode 100644 index 000000000..ed6f8ae95 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java @@ -0,0 +1,1118 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.PlacedObject; +import art.arcane.iris.engine.framework.placer.HeightmapObjectPlacer; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.project.noise.SimplexNoise; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.math.BlockPosition; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterMarker; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.stream.StreamSupport; + +/** + * One-shot placement of an {@link IrisObject} into the world. Instances are cheap and single use: they exist so + * the placement pass can hoist its per-placement invariants without leaking that state onto the loader-cached + * (and thread-shared) object itself. + */ +final class IrisObjectPlacementRunner { + private static final long IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS = 5000L; + private static final long VACUUM_WAVE_SEED = 7392113L; + private static final ConcurrentHashMap IMPLAUSIBLE_BEDROCK_WARNS = new ConcurrentHashMap<>(); + + private final IrisObject self; + + IrisObjectPlacementRunner(IrisObject self) { + this.self = self; + } + + int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer listener, CarveResult c, IrisData rdata) { + IObjectPlacer placer = config.getHeightmap() != null ? new HeightmapObjectPlacer(rng, x, yv, z, config, oplacer) : oplacer; + + if (rdata != null) { + // Slope condition + if (!config.getSlopeCondition().isDefault() && + !config.getSlopeCondition().isValid(rdata.getEngine().getComplex().getSlopeStream().get(x, z)) && !config.isForcePlace()) { + return -1; + } + + // Rotation calculation + int slopeRotationY = 0; + ProceduralStream heightStream = rdata.getEngine().getComplex().getHeightStream(); + if (config.isRotateTowardsSlope()) { + // Whichever side of the rectangle that bounds the object is lowest is the 'direction' of the slope (simply said). + double hNorth = heightStream.get(x, z + ((float) self.d) / 2); + double hEast = heightStream.get(x + ((float) self.w) / 2, z); + double hSouth = heightStream.get(x, z - ((float) self.d) / 2); + double hWest = heightStream.get(x - ((float) self.w) / 2, z); + double min = Math.min(Math.min(hNorth, hEast), Math.min(hSouth, hWest)); + if (min == hNorth) { + slopeRotationY = 0; + } else if (min == hEast) { + slopeRotationY = 90; + } else if (min == hSouth) { + slopeRotationY = 180; + } else if (min == hWest) { + slopeRotationY = 270; + } + + double newRotation = config.getRotation().getYAxis().getMin() + slopeRotationY; + IrisObjectRotation originalRotation = config.getRotation(); + IrisObjectRotation slopeRotation = new IrisObjectRotation(); + slopeRotation.setXAxis(originalRotation.getXAxis()); + slopeRotation.setZAxis(originalRotation.getZAxis()); + if (newRotation == 0) { + slopeRotation.setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90)); + slopeRotation.setEnabled(originalRotation.canRotateX() || originalRotation.canRotateZ()); + } else { + slopeRotation.setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90)); + slopeRotation.setEnabled(true); + } + config = config.toPlacement(config.getPlace().toArray(new String[0])); + config.setRotation(slopeRotation); + } + } + + if (config.isSmartBore()) { + IrisObjectShaping.ensureSmartBored(self, placer.isDebugSmartBore()); + } + + boolean warped = !config.getWarp().isFlat(); + boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE; + boolean organicFloor = config.getMode() == ObjectPlaceMode.ORGANIC_STILT; + boolean ceilingHang = config.getMode() == ObjectPlaceMode.CEILING_HANG; + boolean organic = organicFloor || ceilingHang; + boolean vacuuming = IrisObjectVacuum.isVacuumMode(config.getMode()); + boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) || + config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT || + config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT || organic); + boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT; + KMap heightmap = config.getSnow() > 0 ? new KMap<>() : null; + int spinx = rng.imax() / 1000; + int spiny = rng.imax() / 1000; + int spinz = rng.imax() / 1000; + int rty = config.getRotation().rotate(new IrisBlockVector(0, self.getCenter().getBlockY(), 0), spinx, spiny, spinz).getBlockY(); + int ty = config.getTranslate().translate(new IrisBlockVector(0, self.getCenter().getBlockY(), 0), config.getRotation(), spinx, spiny, spinz).getBlockY(); + // Per-placement invariants. The rotation kernel, the rotated translate offset and the edit-list emptiness + // are pure functions of (config, spin), all of which are fixed from here down. Recomputing them per block + // only cost allocations and transcendentals. + SpinKernel spin = new SpinKernel(config.getRotation(), spinx, spiny, spinz); + IrisObjectTranslate translate = config.getTranslate(); + boolean translating = translate.canTranslate(); + IrisBlockVector translateOffset = translating + ? config.getRotation().rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinx, spiny, spinz) + : null; + boolean hasEdits = !config.getEdit().isEmpty(); + int y = -1; + int xx, zz; + int yrand = config.getTranslate().getYRandom(); + yrand = yrand > 0 ? rng.i(0, yrand) : yrand < 0 ? rng.i(yrand, 0) : yrand; + boolean bail = false; + + if (config.isFromBottom()) { + // todo Convert this to a dedicated mode. + y = (self.getH() + 1) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } else if (yv < 0) { + if (config.getMode().equals(ObjectPlaceMode.CENTER_HEIGHT) || config.getMode() == ObjectPlaceMode.CENTER_STILT + || organic || vacuuming) { + y = (c != null ? c.getSurface() : placer.getHighest(x, z, self.getLoader(), config.isUnderwater())) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.STILT)) { + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + for (int i = minX; i <= maxX; i++) { + for (int ii = minZ; ii <= maxZ; ii++) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h > y) + y = h; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.FAST_MAX_HEIGHT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT)) { + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + + int xRadius = (rotatedDimensions.getBlockX() / 2); + int xLength = xRadius + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zRadius = (rotatedDimensions.getBlockZ() / 2); + int zLength = zRadius + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + + for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { + for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h > y) + y = h; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.MIN_STILT) { + y = rdata.getEngine().getHeight() + 1; + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + + int xLength = (rotatedDimensions.getBlockX() / 2) + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zLength = (rotatedDimensions.getBlockZ() / 2) + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + for (int i = minX; i <= maxX; i++) { + for (int ii = minZ; ii <= maxZ; ii++) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h < y) { + y = h; + } + } + } + } else if (config.getMode().equals(ObjectPlaceMode.FAST_MIN_HEIGHT) || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT) { + y = rdata.getEngine().getHeight() + 1; + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + IrisBlockVector rotatedDimensions = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + + int xRadius = (rotatedDimensions.getBlockX() / 2); + int xLength = xRadius + offset.getBlockX(); + int minX = Math.min(x - xLength, x + xLength); + int maxX = Math.max(x - xLength, x + xLength); + int zRadius = (rotatedDimensions.getBlockZ() / 2); + int zLength = zRadius + offset.getBlockZ(); + int minZ = Math.min(z - zLength, z + zLength); + int maxZ = Math.max(z - zLength, z + zLength); + + for (int i = minX; i <= maxX; i += Math.abs(xRadius) + 1) { + for (int ii = minZ; ii <= maxZ; ii += Math.abs(zRadius) + 1) { + int h = placer.getHighest(i, ii, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, i, h, ii)) { + bail = true; + break; + } + } + if (h < y) { + y = h; + } + } + } + } else if (config.getMode().equals(ObjectPlaceMode.PAINT)) { + y = placer.getHighest(x, z, self.getLoader(), config.isUnderwater()) + rty; + if (!config.isForcePlace()) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } else if (config.getMode().equals(ObjectPlaceMode.FLOATING)) { + y = rty; + } + } else { + y = yv; + if (!config.isForcePlace() && !rawStructurePiece) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } + + if (yv >= 0 && config.isBottom() && !rawStructurePiece) { + y += Math.floorDiv(self.h, 2); + CarvingMode carvingMode = config.getCarvingSupport(); + if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) { + if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { + bail = true; + } + } + } + + if (yv < 0 + && !config.isForcePlace() + && !config.isFromBottom() + && config.getMode() != ObjectPlaceMode.FLOATING + && !rawStructurePiece + && config.getCarvingSupport().supportsSurface() + && placer.getEngine() != null + && placer.getEngine().getDimension().isBedrock() + && y <= 1) { + warnImplausibleBedrockPlacement(placer, config, x, y, z); + return -1; + } + + if (bail && !config.isForcePlace()) { + return -1; + } + + // Surface-anchored placements may never roof or bridge a carved hole. Explicit-Y anchors are only + // guarded for SURFACE_ONLY: ANYWHERE covers the inverted upper dimension and CARVING_ONLY covers + // cave anchors, and neither reads the terrain surface this stencil samples. + boolean surfaceAnchored = yv < 0 + ? config.getCarvingSupport().supportsSurface() + : config.getCarvingSupport() == CarvingMode.SURFACE_ONLY; + if (surfaceAnchored + && !config.isForcePlace() + && !config.isFromBottom() + && config.getMode() != ObjectPlaceMode.FLOATING + && !rawStructurePiece + && !config.isUnderwater() + && !config.isOnwater() + && config.isRequireSurfaceSupport() + && IrisSurfaceSupport.isUnsupported(oplacer, self.getLoader(), x, z, config.getTranslate(), + config.getRotation(), spinx, spiny, spinz, self.getSurfaceSupportOffsets(), + config.getSurfaceSupportBuffer(), config.getSurfaceSupportDepth())) { + return -1; + } + + if (yv < 0 && !config.getMode().equals(ObjectPlaceMode.FLOATING) && !rawStructurePiece) { + if (!config.isForcePlace() && !config.isUnderwater() && !config.isOnwater() && placer.isUnderwater(x, z)) { + return -1; + } + } + + if (!config.isForcePlace() && !rawStructurePiece && c != null && Math.max(0, self.h + yrand + ty) + 1 >= c.getHeight()) { + return -1; + } + + if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { + return -1; + } + + if (!config.isForcePlace() && !rawStructurePiece && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) { + return -1; + } + + if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { + Engine engine = rdata.getEngine(); + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + for (int i = x - Math.floorDiv(self.w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(self.w, 2) - (self.w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { + for (int j = y - Math.floorDiv(self.h, 2) + (int) offset.getY(); j <= y + Math.floorDiv(self.h, 2) - (self.h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { + for (int k = z - Math.floorDiv(self.d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(self.d, 2) - (self.d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) { + PlacedObject p = engine.getObjectPlacement(i, j, k); + if (p == null) continue; + IrisObject o = p.getObject(); + if (o == null) continue; + String key = o.getLoadKey(); + if (key != null) { + if (config.getForbiddenCollisions().contains(key) && !config.getAllowedCollisions().contains(key)) { + return -1; + } + } + } + } + } + } + + if (config.isBore()) { + IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); + for (int i = x - Math.floorDiv(self.w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(self.w, 2) - (self.w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { + for (int j = y - Math.floorDiv(self.h, 2) - config.getBoreExtendMinY() + (int) offset.getY(); j <= y + Math.floorDiv(self.h, 2) + config.getBoreExtendMaxY() - (self.h % 2 == 0 ? 1 : 0) + (int) offset.getY(); j++) { + for (int k = z - Math.floorDiv(self.d, 2) + (int) offset.getZ(); k <= z + Math.floorDiv(self.d, 2) - (self.d % 2 == 0 ? 1 : 0) + (int) offset.getZ(); k++) { + placer.set(i, j, k, IrisObject.States.AIR); + } + } + } + } + + int lowest = Integer.MAX_VALUE; + int topLayer = Integer.MIN_VALUE; + int vacuumLowest = Integer.MAX_VALUE; + int vacuumHighest = Integer.MIN_VALUE; + y += yrand; + self.readLock.lock(); + + KMap markers = null; + + try { + VectorMap blocks = self.blocks; + VectorMap states = self.states; + // Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null. + boolean hasStates = !states.isEmpty(); + + if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) { + markers = new KMap<>(); + var list = StreamSupport.stream(blocks.keys().spliterator(), false) + .collect(KList.collector()); + + for (IrisObjectMarker j : config.getMarkers()) { + IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker()); + + if (marker == null) { + continue; + } + + int max = j.getMaximumMarkers(); + for (IrisBlockVector i : list.shuffle()) { + if (max <= 0) { + break; + } + + PlatformBlockState data = blocks.get(i); + if (data == null) { + continue; + } + + for (PlatformBlockState k : j.getMark(rdata)) { + if (max <= 0) { + break; + } + + if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { + boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0))); + boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0))); + + if (!marker.isEmptyAbove() || (a && fff)) { + markers.put(i, j.getMarker()); + max--; + } + } + } + } + } + } + + VectorMap.Cursor cursor = blocks.cursor(); + while (cursor.next()) { + IrisBlockVector g = cursor.key(); + PlatformBlockState d; + TileData tile = null; + + try { + d = cursor.value(); + if (hasStates) { + tile = states.get(g); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)"); + d = IrisObject.States.AIR; + } + + if (d == null) { + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)"); + d = IrisObject.States.AIR; + } + + PlatformBlockState data = d; + IrisBlockVector i = g.clone(); + spin.rotate(i); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } + if (translating) { + i.add(translateOffset); + } + + if (stilting && IrisObjectShaping.shouldStilt(data)) { + if (i.getBlockY() < lowest) { + lowest = i.getBlockY(); + } + if (i.getBlockY() > topLayer) { + topLayer = i.getBlockY(); + } + } + + if (placer.isPreventingDecay() && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) { + data = data.withProperty("persistent", "true"); + } + + if (hasEdits) { + for (IrisObjectReplace j : config.getEdit()) { + if (rng.chance(j.getChance())) { + for (PlatformBlockState k : j.getFind(rdata)) { + if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { + PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); + + if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)) && !(newData.isCustom() || data.isCustom())) + data = BlockDataMergeSupport.merge(data, newData); + else + data = newData; + + Optional t = j.getReplace().getTile(rng, x, y, z, rdata); + if (t.isPresent()) { + tile = t.get(); + } + } + } + } + } + } + + data = config.getRotation().rotate(data, spinx, spiny, spinz); + xx = x + (int) Math.round(i.getX()); + + int yy = y + (int) Math.round(i.getY()); + zz = z + (int) Math.round(i.getZ()); + + if (warped) { + xx += config.warp(rng, i.getX() + x, i.getY() + y, i.getZ() + z, self.getLoader()); + zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, self.getLoader()); + } + + if (yv < 0 && (config.getMode().equals(ObjectPlaceMode.PAINT)) && !B.isVineBlock(data)) { + yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()); + } + + if (heightmap != null) { + Position2 pos = new Position2(xx, zz); + + if (!heightmap.containsKey(pos)) { + heightmap.put(pos, yy); + } + + if (heightmap.get(pos) < yy) { + heightmap.put(pos, yy); + } + } + + if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { + continue; + } + + if (IrisProceduralBlocks.hasProperty(data, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) { + data = data.withProperty("waterlogged", "true"); + } + + if (B.isVineBlock(data)) { + data = attachVineFaces(placer, data, xx, yy, zz); + } + + PlatformBlockState existingState = placer.get(xx, yy, zz); + boolean wouldReplace = B.isSolid(existingState) && B.isVineBlock(data); + String material = IrisObjectShaping.materialKey(data); + boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air"); + boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace); + + if (data.isCustom() || place) { + placer.set(xx, yy, zz, data); + if (tile != null) { + placer.setTile(xx, yy, zz, tile); + } + if (markers != null && markers.containsKey(g)) { + placer.setData(xx, yy, zz, new MatterMarker(markers.get(g))); + } + if (listener != null) { + listener.accept(new BlockPosition(xx, yy, zz), data); + } + if (vacuuming && yy < vacuumLowest) { + vacuumLowest = yy; + } + if (vacuuming && yy > vacuumHighest) { + vacuumHighest = yy; + } + } + } + } catch (Throwable e) { + e.printStackTrace(); + IrisLogging.reportError(e); + } finally { + self.readLock.unlock(); + } + + if (stilting) { + self.readLock.lock(); + try { + VectorMap blocks = self.blocks; + IrisStiltSettings settings = config.getStiltSettings(); + + double erodeCentroidX = 0; + double erodeCentroidZ = 0; + double erodeMaxDist = 1; + if (eroding) { + int centroidCount = 0; + VectorMap.Cursor centroidCursor = blocks.cursor(); + while (centroidCursor.next()) { + IrisBlockVector rot = centroidCursor.key().clone(); + spin.rotate(rot); + if (translating) { + rot.add(translateOffset); + } + if (rot.getBlockY() == lowest) { + PlatformBlockState bd = centroidCursor.value(); + if (bd != null && IrisObjectShaping.shouldStilt(bd)) { + erodeCentroidX += rot.getX(); + erodeCentroidZ += rot.getZ(); + centroidCount++; + } + } + } + if (centroidCount > 0) { + erodeCentroidX /= centroidCount; + erodeCentroidZ /= centroidCount; + } + VectorMap.Cursor spreadCursor = blocks.cursor(); + while (spreadCursor.next()) { + IrisBlockVector rot = spreadCursor.key().clone(); + spin.rotate(rot); + if (translating) { + rot.add(translateOffset); + } + if (rot.getBlockY() == lowest) { + PlatformBlockState bd = spreadCursor.value(); + if (bd != null && IrisObjectShaping.shouldStilt(bd)) { + double dx = rot.getX() - erodeCentroidX; + double dz = rot.getZ() - erodeCentroidZ; + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > erodeMaxDist) { + erodeMaxDist = dist; + } + } + } + } + } + + VectorMap.Cursor stiltCursor = blocks.cursor(); + while (stiltCursor.next()) { + IrisBlockVector g = stiltCursor.key(); + PlatformBlockState sourceData; + try { + sourceData = stiltCursor.value(); + } catch (Throwable e) { + IrisLogging.reportError(e); + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)"); + sourceData = IrisObject.States.AIR; + } + + if (sourceData == null) { + IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)"); + sourceData = IrisObject.States.AIR; + } + + if (!IrisObjectShaping.shouldStilt(sourceData)) { + continue; + } + + PlatformBlockState d = sourceData; + if (settings != null && settings.getPalette() != null) { + d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); + } else { + String mat = IrisObjectShaping.materialKey(d); + if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) { + d = B.getState("minecraft:dirt"); + } + } + + IrisBlockVector i = g.clone(); + spin.rotate(i); + if (ceilingHang) { + i.setY(-i.getBlockY()); + } + if (translating) { + i.add(translateOffset); + } + d = config.getRotation().rotate(d, spinx, spiny, spinz); + + int targetLayer = ceilingHang ? topLayer : lowest; + if (i.getBlockY() != targetLayer) + continue; + + if (hasEdits) { + for (IrisObjectReplace j : config.getEdit()) { + if (rng.chance(j.getChance())) { + for (PlatformBlockState k : j.getFind(rdata)) { + if (d == null) { + continue; + } + if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) { + PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); + + if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) { + d = BlockDataMergeSupport.merge(d, newData); + } else { + d = newData; + } + } + } + } + } + } + + if (d == null || !d.isOccluding()) + continue; + + xx = x + (int) Math.round(i.getX()); + zz = z + (int) Math.round(i.getZ()); + + if (warped) { + xx += config.warp(rng, i.getX() + x, i.getY() + y, i.getZ() + z, self.getLoader()); + zz += config.warp(rng, i.getZ() + z, i.getY() + y, i.getX() + x, self.getLoader()); + } + + if (organic) { + int startY = targetLayer + y; + int maxScan = settings != null ? Math.max(1, settings.getOrganicMaxScan()) : 48; + int jitterMax = settings != null ? Math.max(0, settings.getOrganicJitter()) : 3; + double scratch = settings != null ? Math.max(0, Math.min(1, settings.getOrganicScratch())) : 0.55; + long colHash = ((long) xx * 341873128712L) ^ ((long) zz * 132897987541L); + int jitter = jitterMax > 0 ? (int) (Math.abs(colHash) % (jitterMax + 1)) : 0; + + if (ceilingHang) { + int scan = 0; + int solidY = startY + 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY++; + scan++; + } + int topBound = (scan < maxScan ? solidY - 1 : startY + Math.min(maxScan, 8)) - jitter; + int total = topBound - startY; + for (int j = startY; j <= topBound; j++) { + if (scratch > 0 && total > 0) { + double ratio = (double) (j - startY) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, d); + } + } else { + int scan = 0; + int solidY = startY - 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY--; + scan++; + } + int bottomBound = (scan < maxScan ? solidY + 1 : startY - Math.min(maxScan, 8)) + jitter; + int total = startY - bottomBound; + for (int j = startY; j >= bottomBound; j--) { + if (scratch > 0 && total > 0) { + double ratio = (double) (startY - j) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, d); + } + } + continue; + } + + int highest = placer.getHighest(xx, zz, self.getLoader(), true); + + if (IrisProceduralBlocks.hasProperty(d, "waterlogged") && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) { + d = d.withProperty("waterlogged", "true"); + } + + int lowerBound = highest - 1; + if (settings != null) { + lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); + if (settings.getYMax() != 0) + lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); + } + + if (eroding) { + double dx = i.getX() - erodeCentroidX; + double dz = i.getZ() - erodeCentroidZ; + double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; + normalizedDist = Math.min(normalizedDist, 1.0); + int totalDepth = (lowest + y) - lowerBound; + int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5)); + lowerBound = (lowest + y) - erodeDepth; + } + + for (int j = lowest + y; j > lowerBound; j--) { + PlatformBlockState fluidState = placer.get(xx, j, zz); + if (B.isFluid(fluidState)) { + break; + } + if (eroding) { + int depth = (lowest + y) - j; + int totalDepth = (lowest + y) - lowerBound; + double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0; + if (depthRatio > 0.4) { + long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L)); + double skipChance = (depthRatio - 0.4) / 0.6; + if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + + if (B.isVineBlock(d)) { + d = attachVineFaces(placer, d, xx, j, zz); + } + placer.set(xx, j, zz, d); + } + + } + } finally { + self.readLock.unlock(); + } + } + + if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) { + IrisBlockVector rotDim = config.getRotation().rotate(new IrisBlockVector(self.getW(), self.getH(), self.getD()), spinx, spiny, spinz).clone(); + int lowX = IrisObjectVacuum.footprintLow(rotDim.getBlockX()); + int highX = IrisObjectVacuum.footprintHigh(rotDim.getBlockX()); + int lowZ = IrisObjectVacuum.footprintLow(rotDim.getBlockZ()); + int highZ = IrisObjectVacuum.footprintHigh(rotDim.getBlockZ()); + int centerX = x + config.getTranslate().getX(); + int centerZ = z + config.getTranslate().getZ(); + vacuumTerrain(placer, config, centerX, centerZ, lowX, highX, lowZ, highZ, vacuumLowest, vacuumHighest); + } + + if (heightmap != null) { + RNG rngx = rng.nextParallelRNG(3468854); + + for (Position2 i : heightmap.k()) { + int vx = i.getX(); + int vy = heightmap.get(i); + int vz = i.getZ(); + + if (config.getSnow() > 0) { + int height = rngx.i(0, (int) (config.getSnow() * 7)); + placer.set(vx, vy + 1, vz, IrisObject.States.SNOW_LAYERS[Math.max(Math.min(height, 7), 0)]); + } + } + } + + return y; + } + + static boolean shouldPlaceObjectBlock(boolean rawStructurePiece, boolean air, boolean wouldReplace) { + return !wouldReplace && (rawStructurePiece || !air); + } + + private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) { + String key = self.getLoadKey(); + String fingerprint = (key == null ? "" : key) + "|" + config.getMode(); + long now = System.currentTimeMillis(); + Long last = IMPLAUSIBLE_BEDROCK_WARNS.get(fingerprint); + if (last != null && now - last < IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS) { + return; + } + IMPLAUSIBLE_BEDROCK_WARNS.put(fingerprint, now); + IrisLogging.warn("Implausible object placement rejected: " + + (key == null ? "" : key) + + " resolved anchorY=" + y + " at (" + x + "," + z + ") mode=" + config.getMode() + + " carving=" + config.getCarvingSupport() + + ". Surface-anchored placement should never land on the bedrock row. " + + "Height sampling returned a bogus value — not configured for floor placement " + + "(forcePlace=false, fromBottom=false, mode!=FLOATING). Skipping to protect bedrock."); + } + + private void vacuumTerrain(IObjectPlacer placer, IrisObjectPlacement config, int centerX, int centerZ, int lowX, int highX, int lowZ, int highZ, int baseY, int topY) { + ObjectPlaceMode mode = config.getMode(); + IrisVacuumSettings settings = config.getVacuumSettings(); + int radius = IrisObjectVacuum.resolveRadius(mode, settings); + int step = IrisObjectVacuum.resolveStep(mode); + double falloff = IrisObjectVacuum.resolveFalloff(settings); + int jitter = settings != null ? Math.max(0, settings.getOrganicJitter()) : 4; + boolean organicEdge = mode == ObjectPlaceMode.VACUUM_ORGANIC; + boolean wavyEdge = mode == ObjectPlaceMode.VACUUM_WAVY; + double waveAmplitude = IrisObjectVacuum.resolveWaveAmplitude(settings); + double waveScale = IrisObjectVacuum.resolveWaveScale(settings); + SimplexNoise waveNoise = (wavyEdge && waveAmplitude > 0) ? new SimplexNoise(VACUUM_WAVE_SEED) : null; + int meetY = baseY - 1; + + IrisComplex complex = placer.getEngine().getComplex(); + int worldMin = placer.getEngine().getMinHeight(); + int worldMax = worldMin + placer.getEngine().getHeight() - 1; + + for (int dx = lowX - radius; dx <= highX + radius; dx += step) { + for (int dz = lowZ - radius; dz <= highZ + radius; dz += step) { + int cx = centerX + dx; + int cz = centerZ + dz; + double effRadius = radius; + if (organicEdge && jitter > 0) { + long h = ((long) cx * 341873128712L) ^ ((long) cz * 132897987541L); + double n = ((Math.abs(h) % 1000) / 1000.0) - 0.5; + effRadius = Math.max(1.0, radius + (n * 2.0 * jitter)); + } + int origY = placer.getHighest(cx, cz, self.getLoader(), true); + int targetY = IrisObjectVacuum.columnTargetY(dx, dz, lowX, highX, lowZ, highZ, effRadius, falloff, origY, meetY); + if (waveNoise != null) { + int outX = IrisObjectVacuum.outset(dx, lowX, highX); + int outZ = IrisObjectVacuum.outset(dz, lowZ, highZ); + double waveDistance = Math.sqrt((double) (outX * outX) + (double) (outZ * outZ)); + double sample = waveNoise.noiseSigned(cx * waveScale, cz * waveScale); + targetY += IrisObjectVacuum.waveOffset(waveDistance, effRadius, sample, waveAmplitude); + } + if (targetY == origY) { + continue; + } + targetY = Math.max(worldMin + 1, Math.min(worldMax, targetY)); + if (targetY > origY) { + PlatformBlockState fill = complex != null ? complex.getRockStream().get(cx, cz) : null; + if (B.isAir(fill)) { + fill = IrisObject.States.STONE; + } + for (int yy = origY + 1; yy <= targetY; yy++) { + placer.set(cx, yy, cz, fill); + } + } else if (targetY < origY) { + boolean inside = IrisObjectVacuum.outset(dx, lowX, highX) == 0 && IrisObjectVacuum.outset(dz, lowZ, highZ) == 0; + int carveFloor = IrisObjectVacuum.carveFloorY(targetY, topY, inside); + for (int yy = origY; yy >= carveFloor; yy--) { + placer.set(cx, yy, cz, IrisObject.States.AIR); + } + } + } + } + } + + private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) { + CarvingMode carvingMode = placement.getCarvingSupport(); + return switch (carvingMode) { + case SURFACE_ONLY -> placer.isCarved(x, y, z); + case CARVING_ONLY -> !isCarvedCaveAnchor(placer, x, y, z); + case ANYWHERE -> false; + }; + } + + private boolean isCarvedCaveAnchor(IObjectPlacer placer, int x, int y, int z) { + return placer.isCarved(x, y, z) + || placer.isCarved(x, y - 1, z) + || placer.isCarved(x, y - 2, z) + || placer.isCarved(x, y - 3, z); + } + + private boolean shouldAutoWaterlogBlock(IObjectPlacer placer, IrisObjectPlacement placement, int yv, int x, int y, int z) { + if (!(placement.isWaterloggable() || placement.isUnderwater())) { + return false; + } + + if (yv >= 0 && placement.getCarvingSupport().equals(CarvingMode.CARVING_ONLY)) { + return false; + } + + PlatformBlockState existing = placer.get(x, y, z); + if (existing == null) { + return false; + } + + return B.isWater(existing) || B.isWaterLogged(existing); + } + + private static PlatformBlockState attachVineFaces(IObjectPlacer placer, PlatformBlockState data, int x, int y, int z) { + PlatformBlockState result = data; + for (String face : IrisProceduralBlocks.FACE_PROPERTIES) { + if (!IrisProceduralBlocks.hasProperty(data, face)) { + continue; + } + int[] mod = IrisProceduralBlocks.faceOffset(face); + PlatformBlockState facing = placer.get(x + mod[0], y + mod[1], z + mod[2]); + if (B.isSolid(facing) && !B.isVineBlock(facing)) { + result = result.withProperty(face, "true"); + } + } + return result; + } + + /** + * Precomputed vector rotation for a single placement. + *

+ * This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two + * differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine + * and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the + * spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to + * the value the per-call form would have produced. + */ + private static final class SpinKernel { + private static final int MODE_NONE = 0; + private static final int MODE_FLIP = 1; + private static final int MODE_QUARTER = 2; + private static final int MODE_THREE_QUARTER = 3; + private static final int MODE_ANGLE = 4; + + private final boolean rotates; + private final int xMode; + private final int yMode; + private final int zMode; + private final double xCos; + private final double xSin; + private final double yCos; + private final double ySin; + private final double zCos; + private final double zSin; + + private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) { + rotates = rotation.canRotate(); + xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE; + zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE; + yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE; + + double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0; + double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0; + double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0; + xCos = Math.cos(xAngle); + xSin = Math.sin(xAngle); + zCos = Math.cos(zAngle); + zSin = Math.sin(zAngle); + yCos = Math.cos(yAngle); + ySin = Math.sin(yAngle); + } + + private static int modeOf(IrisAxisRotationClamp clamp) { + if (!clamp.isLocked()) { + return MODE_ANGLE; + } + + if (Math.abs(clamp.getMax()) % 360D == 180D) { + return MODE_FLIP; + } + + if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) { + return MODE_QUARTER; + } + + if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) { + return MODE_THREE_QUARTER; + } + + return MODE_ANGLE; + } + + /** + * Rotates in place. Axis order (X, then Z, then Y) is load bearing. + */ + private void rotate(IrisBlockVector v) { + if (!rotates) { + return; + } + + switch (xMode) { + case MODE_FLIP -> { + v.setZ(-v.getZ()); + v.setY(-v.getY()); + } + case MODE_QUARTER -> { + double z = v.getZ(); + v.setZ(v.getY()); + v.setY(-z); + } + case MODE_THREE_QUARTER -> { + double z = v.getZ(); + v.setZ(-v.getY()); + v.setY(z); + } + case MODE_ANGLE -> { + double y = xCos * v.getY() - xSin * v.getZ(); + double z = xSin * v.getY() + xCos * v.getZ(); + v.setY(y); + v.setZ(z); + } + default -> { + } + } + + switch (zMode) { + case MODE_FLIP -> { + v.setY(-v.getY()); + v.setX(-v.getX()); + } + case MODE_QUARTER -> { + double y = v.getY(); + v.setY(v.getX()); + v.setX(-y); + } + case MODE_THREE_QUARTER -> { + double y = v.getY(); + v.setY(-v.getX()); + v.setX(y); + } + case MODE_ANGLE -> { + double x = zCos * v.getX() - zSin * v.getY(); + double y = zSin * v.getX() + zCos * v.getY(); + v.setX(x); + v.setY(y); + } + default -> { + } + } + + switch (yMode) { + case MODE_FLIP -> { + v.setX(-v.getX()); + v.setZ(-v.getZ()); + } + case MODE_QUARTER -> { + double x = v.getX(); + v.setX(v.getZ()); + v.setZ(-x); + } + case MODE_THREE_QUARTER -> { + double x = v.getX(); + v.setX(-v.getZ()); + v.setZ(x); + } + case MODE_ANGLE -> { + double x = yCos * v.getX() + ySin * v.getZ(); + double z = -ySin * v.getX() + yCos * v.getZ(); + v.setX(x); + v.setZ(z); + } + default -> { + } + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java index 4098a475c..a2094c482 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectRotation.java @@ -508,7 +508,7 @@ public class IrisObjectRotation { v.rotateAroundZ(getZRotation(spinz)); } } else { - v.rotateAroundY(getZRotation(spinz)); + v.rotateAroundZ(getZRotation(spinz)); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java new file mode 100644 index 000000000..cf1d94865 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java @@ -0,0 +1,279 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.IrisVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block + * classification helpers used by placement. + */ +final class IrisObjectShaping { + private IrisObjectShaping() { + } + + static void ensureSmartBored(IrisObject self, boolean debug) { + if (self.smartBored) { + return; + } + + PrecisionStopwatch p = PrecisionStopwatch.start(); + PlatformBlockState vair = debug ? IrisObject.States.VAIR_DEBUG : IrisObject.States.VAIR; + AtomicInteger applied = new AtomicInteger(); + IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); + IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); + VectorMap source; + self.readLock.lock(); + try { + if (self.blocks.isEmpty()) { + IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it."); + self.smartBored = true; + return; + } + + source = self.blocks; + + for (IrisBlockVector i : self.blocks.keys()) { + max.setX(Math.max(i.getX(), max.getX())); + min.setX(Math.min(i.getX(), min.getX())); + max.setY(Math.max(i.getY(), max.getY())); + min.setY(Math.min(i.getY(), min.getY())); + max.setZ(Math.max(i.getZ(), max.getZ())); + min.setZ(Math.min(i.getZ(), min.getZ())); + } + } finally { + self.readLock.unlock(); + } + + VectorMap bore = new VectorMap<>(); + BurstExecutor burst = MultiBurst.burst.burst(); + + // Smash X + for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { + int finalRayY = rayY; + burst.queue(() -> { + for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + + for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(ray, finalRayY, rayZ))) { + start = Math.min(ray, start); + end = Math.max(ray, end); + } + } + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + boreCell(source, bore, new IrisBlockVector(i, finalRayY, rayZ), vair, applied); + } + } + } + }); + } + + // Smash Y + for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { + int finalRayX = rayX; + burst.queue(() -> { + for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + + for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(finalRayX, ray, rayZ))) { + start = Math.min(ray, start); + end = Math.max(ray, end); + } + } + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + boreCell(source, bore, new IrisBlockVector(finalRayX, i, rayZ), vair, applied); + } + } + } + }); + } + + // Smash Z + for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { + int finalRayX = rayX; + burst.queue(() -> { + for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + + for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(finalRayX, rayY, ray))) { + start = Math.min(ray, start); + end = Math.max(ray, end); + } + } + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + boreCell(source, bore, new IrisBlockVector(finalRayX, rayY, i), vair, applied); + } + } + } + }); + } + + burst.complete(); + + self.writeLock.lock(); + try { + if (!self.smartBored) { + bore.forEach((v, s) -> self.blocks.computeIfAbsent(v, (vv) -> s)); + self.smartBored = true; + } + } finally { + self.writeLock.unlock(); + } + + IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")"); + } + + private static boolean boreContains(VectorMap source, VectorMap bore, IrisBlockVector v) { + return source.containsKey(v) || bore.containsKey(v); + } + + private static void boreCell(VectorMap source, VectorMap bore, IrisBlockVector v, PlatformBlockState vair, AtomicInteger applied) { + PlatformBlockState existing = source.get(v); + + if (existing == null) { + if (vair.equals(bore.get(v))) { + return; + } + + bore.computeIfAbsent(v, (vv) -> vair); + } else if (vair.equals(existing)) { + return; + } + + applied.getAndIncrement(); + } + + static void shrinkwrap(IrisObject self) { + if (self.blocks.isEmpty()) return; + IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + + for (IrisBlockVector i : self.blocks.keys()) { + min.setX(Math.min(min.getX(), i.getX())); + min.setY(Math.min(min.getY(), i.getY())); + min.setZ(Math.min(min.getZ(), i.getZ())); + max.setX(Math.max(max.getX(), i.getX())); + max.setY(Math.max(max.getY(), i.getY())); + max.setZ(Math.max(max.getZ(), i.getZ())); + } + + self.w = max.getBlockX() - min.getBlockX() + 1; + self.h = max.getBlockY() - min.getBlockY() + 1; + self.d = max.getBlockZ() - min.getBlockZ() + 1; + self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2); + + Vector3i offset = new Vector3i( + -self.center.getBlockX() - min.getBlockX(), + -self.center.getBlockY() - min.getBlockY(), + -self.center.getBlockZ() - min.getBlockZ() + ); + if (offset.getBlockX() == 0 && offset.getBlockY() == 0 && offset.getBlockZ() == 0) + return; + + VectorMap b = new VectorMap<>(); + VectorMap s = new VectorMap<>(); + IrisBlockVector shift = new IrisBlockVector(offset.getX(), offset.getY(), offset.getZ()); + + self.blocks.forEach((vector, data) -> { + vector.add(shift); + b.put(vector, data); + }); + + self.states.forEach((vector, data) -> { + vector.add(shift); + s.put(vector, data); + }); + + self.shrinkOffset = offset; + self.blocks = b; + self.states = s; + self.surfaceSupportOffsets.reset(); + } + + static void clean(IrisObject self) { + VectorMap d = new VectorMap<>(); + d.putAll(self.blocks); + + VectorMap dx = new VectorMap<>(); + dx.putAll(self.states); + + self.blocks = d; + self.states = dx; + self.surfaceSupportOffsets.reset(); + } + + static boolean shouldStilt(PlatformBlockState state) { + if (!state.isOccluding()) { + return false; + } + String material = materialKey(state); + if (material.endsWith("_stairs") || material.endsWith("_slab")) { + return false; + } + return !material.equals("minecraft:dirt_path"); + } + + static String materialKey(PlatformBlockState state) { + return IrisProceduralBlocks.materialKey(state); + } + + static List blocksBetweenTwoPoints(IrisVector loc1, IrisVector loc2) { + List locations = new ArrayList<>(); + int topBlockX = Math.max(loc1.getBlockX(), loc2.getBlockX()); + int bottomBlockX = Math.min(loc1.getBlockX(), loc2.getBlockX()); + int topBlockY = Math.max(loc1.getBlockY(), loc2.getBlockY()); + int bottomBlockY = Math.min(loc1.getBlockY(), loc2.getBlockY()); + int topBlockZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); + int bottomBlockZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ()); + + for (int x = bottomBlockX; x <= topBlockX; x++) { + for (int z = bottomBlockZ; z <= topBlockZ; z++) { + for (int y = bottomBlockY; y <= topBlockY; y++) { + locations.add(new IrisBlockVector(x, y, z)); + } + } + } + return locations; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java new file mode 100644 index 000000000..7b659508a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTransforms.java @@ -0,0 +1,269 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.data.VectorMap; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.IrisVector; +import art.arcane.iris.util.project.interpolation.Interpolation3D; + +/** + * Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers. + */ +final class IrisObjectTransforms { + private IrisObjectTransforms() { + } + + static IrisObject rotateCopy(IrisObject self, IrisObjectRotation rt) { + IrisObject copy = self.copy(); + rotate(copy, rt, 0, 0, 0); + return copy; + } + + static void rotate(IrisObject self, IrisObjectRotation r, int spinx, int spiny, int spinz) { + self.writeLock.lock(); + try { + VectorMap d = new VectorMap<>(); + + for (var entry : self.blocks) { + d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz)); + } + + VectorMap dx = new VectorMap<>(); + + for (var entry : self.states) { + dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue()); + } + + self.blocks = d; + self.states = dx; + IrisObjectShaping.shrinkwrap(self); + self.surfaceSupportOffsets.reset(); + } finally { + self.writeLock.unlock(); + } + } + + static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) { + if (interpolation == null) { + interpolation = IrisObjectPlacementScaleInterpolator.NONE; + } + IrisVector sm1 = new IrisVector(scale - 1, scale - 1, scale - 1); + scale = Math.max(0.001, Math.min(50, scale)); + if (scale < 1) { + scale = scale - 0.0001; + } + + IrisPosition l1 = self.getAABB().max(); + IrisPosition l2 = self.getAABB().min(); + VectorMap placeBlock = new VectorMap<>(); + + IrisVector center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ()); + if (self.getH() == 2) { + center = center.setY(center.getBlockY() + 0.5); + } + if (self.getW() == 2) { + center = center.setX(center.getBlockX() + 0.5); + } + if (self.getD() == 2) { + center = center.setZ(center.getBlockZ() + 0.5); + } + + IrisObject oo = new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2))); + oo.setLoadKey(self.getLoadKey()); + oo.setLoader(self.getLoader()); + oo.setLoadFile(self.getLoadFile()); + + self.readLock.lock(); + try { + for (var entry : self.blocks) { + PlatformBlockState bd = entry.getValue(); + placeBlock.put(entry.getKey().clone().add(IrisObject.HALF).subtract(center) + .multiply(scale).add(sm1).toBlockVector(), bd); + } + } finally { + self.readLock.unlock(); + } + + for (var entry : placeBlock) { + IrisBlockVector v = entry.getKey(); + if (scale > 1) { + for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(v.clone().add(center), v.clone().add(center).add(sm1))) { + oo.blocks.put(vec, entry.getValue()); + } + } else { + oo.setUnsigned(v.getBlockX(), v.getBlockY(), v.getBlockZ(), entry.getValue()); + } + } + + if (scale > 1) { + switch (interpolation) { + case TRILINEAR -> trilinear(oo, (int) Math.round(scale)); + case TRICUBIC -> tricubic(oo, (int) Math.round(scale)); + case TRIHERMITE -> trihermite(oo, (int) Math.round(scale)); + } + } + + return oo; + } + + static void trilinear(IrisObject self, int rad) { + self.writeLock.lock(); + try { + VectorMap v = self.blocks; + VectorMap b = new VectorMap<>(); + IrisPosition min = self.getAABB().min(); + IrisPosition max = self.getAABB().max(); + + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + if (Interpolation3D.getTrilinear(x, y, z, rad, (xx, yy, zz) -> { + PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); + + if (B.isAir(data)) { + return 0; + } + + return 1; + }) >= 0.5) { + b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); + } else { + b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); + } + } + } + } + + self.blocks = b; + self.surfaceSupportOffsets.reset(); + } finally { + self.writeLock.unlock(); + } + } + + static void tricubic(IrisObject self, int rad) { + self.writeLock.lock(); + try { + VectorMap v = self.blocks; + VectorMap b = new VectorMap<>(); + IrisPosition min = self.getAABB().min(); + IrisPosition max = self.getAABB().max(); + + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + if (Interpolation3D.getTricubic(x, y, z, rad, (xx, yy, zz) -> { + PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); + + if (B.isAir(data)) { + return 0; + } + + return 1; + }) >= 0.5) { + b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); + } else { + b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); + } + } + } + } + + self.blocks = b; + self.surfaceSupportOffsets.reset(); + } finally { + self.writeLock.unlock(); + } + } + + static void trihermite(IrisObject self, int rad) { + trihermite(self, rad, 0D, 0D); + } + + static void trihermite(IrisObject self, int rad, double tension, double bias) { + self.writeLock.lock(); + try { + VectorMap v = self.blocks; + VectorMap b = new VectorMap<>(); + IrisPosition min = self.getAABB().min(); + IrisPosition max = self.getAABB().max(); + + for (int x = min.getX(); x <= max.getX(); x++) { + for (int y = min.getY(); y <= max.getY(); y++) { + for (int z = min.getZ(); z <= max.getZ(); z++) { + if (Interpolation3D.getTrihermite(x, y, z, rad, (xx, yy, zz) -> { + PlatformBlockState data = v.get(new IrisBlockVector((int) xx, (int) yy, (int) zz)); + + if (B.isAir(data)) { + return 0; + } + + return 1; + }, tension, bias) >= 0.5) { + b.put(new IrisBlockVector(x, y, z), nearestBlockData(self, x, y, z)); + } else { + b.put(new IrisBlockVector(x, y, z), IrisObject.States.AIR); + } + } + } + } + + self.blocks = b; + self.surfaceSupportOffsets.reset(); + } finally { + self.writeLock.unlock(); + } + } + + private static PlatformBlockState nearestBlockData(IrisObject self, int x, int y, int z) { + IrisBlockVector vv = new IrisBlockVector(x, y, z); + self.readLock.lock(); + try { + PlatformBlockState r = self.blocks.get(vv); + + if (!B.isAir(r)) { + return r; + } + + double d = Double.MAX_VALUE; + + for (var entry : self.blocks) { + PlatformBlockState dat = entry.getValue(); + + if (B.isAir(dat)) { + continue; + } + + double dx = entry.getKey().distanceSquared(vv); + + if (dx < d) { + d = dx; + r = dat; + } + } + + return r; + } finally { + self.readLock.unlock(); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java index 2b9a67795..ce77a67ab 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectTranslate.java @@ -36,24 +36,24 @@ import lombok.experimental.Accessors; @Desc("Translate objects") @Data public class IrisObjectTranslate { - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT + @MinNumber(-128) + @MaxNumber(128) @Desc("The x shift in blocks") private int x = 0; @Required - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT + @MinNumber(-128) + @MaxNumber(128) @Desc("The y shift in blocks") private int y = 0; - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT + @MinNumber(-128) + @MaxNumber(128) @Desc("Adds an additional amount of height randomly (translateY + rand(0 - yRandom))") private int yRandom = 0; - @MinNumber(-128) // TODO: WARNING HEIGHT - @MaxNumber(128) // TODO: WARNING HEIGHT + @MinNumber(-128) + @MaxNumber(128) @Desc("The z shift in blocks") private int z = 0; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPosition2D.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPosition2D.java deleted file mode 100644 index 8864b306b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisPosition2D.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("position-2d") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a position") -@Data -public class IrisPosition2D { - @Desc("The x position") - private int x = 0; - - @Desc("The z position") - private int z = 0; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java index fc994a800..845d57556 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralBlocks.java @@ -65,10 +65,20 @@ public final class IrisProceduralBlocks { } public static boolean hasProperty(PlatformBlockState state, String property) { - return propertyValue(state, property) != null; + String key = state.key(); + int start = propertyStart(key, property); + if (start < 0) { + return false; + } + int valueStart = start + property.length() + 2; + return key.indexOf(',', valueStart) >= 0 || key.indexOf(']', valueStart) >= 0; } public static String materialKey(PlatformBlockState state) { + String memoized = state.materialKey(); + if (memoized != null) { + return memoized; + } String key = state.key(); int bracket = key.indexOf('['); return bracket < 0 ? key : key.substring(0, bracket); @@ -76,18 +86,11 @@ public final class IrisProceduralBlocks { public static String propertyValue(PlatformBlockState state, String property) { String key = state.key(); - int bracket = key.indexOf('['); - if (bracket < 0) { - return null; - } - int start = key.indexOf("[" + property + "=", bracket); - if (start < 0) { - start = key.indexOf("," + property + "=", bracket); - } + int start = propertyStart(key, property); if (start < 0) { return null; } - int valueStart = key.indexOf('=', start) + 1; + int valueStart = start + property.length() + 2; int end = key.indexOf(',', valueStart); if (end < 0) { end = key.indexOf(']', valueStart); @@ -98,6 +101,30 @@ public final class IrisProceduralBlocks { return key.substring(valueStart, end); } + /** + * Index of the delimiter opening "[property=" or ",property=" within the key, or -1. Scans in place + * rather than building the search needles, and prefers a "[" match over a "," match. + */ + private static int propertyStart(String key, String property) { + int bracket = key.indexOf('['); + if (bracket < 0) { + return -1; + } + int start = delimitedPropertyIndex(key, property, bracket, '['); + return start < 0 ? delimitedPropertyIndex(key, property, bracket, ',') : start; + } + + private static int delimitedPropertyIndex(String key, String property, int from, char delimiter) { + int length = property.length(); + int limit = key.length() - length - 2; + for (int i = key.indexOf(delimiter, from); i >= 0 && i <= limit; i = key.indexOf(delimiter, i + 1)) { + if (key.regionMatches(i + 1, property, 0, length) && key.charAt(i + 1 + length) == '=') { + return i; + } + } + return -1; + } + public static IrisObject assemble(Map blocks) { if (blocks == null || blocks.isEmpty()) { return null; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java deleted file mode 100644 index c58c95b0f..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRareObject.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.iris.engine.object.annotations.Required; -import art.arcane.iris.engine.object.annotations.Snippet; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("rare-object") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a structure tile") -@Data -@EqualsAndHashCode(callSuper = false) -public class IrisRareObject { - @Required - @MinNumber(1) - @Desc("The rarity is 1 in X") - private int rarity = 1; - - @RegistryListResource(IrisObject.class) - @Required - @Desc("The object to place if rarity check passed") - private String object = ""; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSeed.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSeed.java deleted file mode 100644 index 82e660325..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisSeed.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.Snippet; -import art.arcane.volmlib.util.math.RNG; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("color") -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Represents a color") -@Data -public class IrisSeed { - @Desc("The seed to use") - private long seed = 1337; - - @Desc("To calculate a seed Iris passes in it's natural seed for the current feature, then mixes it with your seed. Setting this to true ignores the parent seed and always uses your exact seed ignoring the input of Iris feature seeds. You can use this to match seeds on other generators.") - private boolean ignoreNaturalSeedInput = false; - - public long getSeed(long seed) { - return (seed * 47) + getSeed() + 29334667L; - } - - public RNG rng(long inseed) { - return new RNG(getSeed(inseed)); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java index 9cc66ac5f..0aee404c9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java @@ -42,14 +42,14 @@ public class IrisShapedGeneratorStyle { private IrisGeneratorStyle generator = new IrisGeneratorStyle(NoiseStyle.IRIS); @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MinNumber(-2032) + @MaxNumber(2032) @Desc("The min block value") private int min = 0; @Required - @MinNumber(-2032) // TODO: WARNING HEIGHT - @MaxNumber(2032) // TODO: WARNING HEIGHT + @MinNumber(-2032) + @MaxNumber(2032) @Desc("The max block value") private int max = 0; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisWorm.java b/core/src/main/java/art/arcane/iris/engine/object/IrisWorm.java deleted file mode 100644 index 5f241c035..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisWorm.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.Snippet; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KSet; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.noise.CNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - - -@Snippet("worm") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Generate worms") -@Data -public class IrisWorm { - @Desc("The style used to determine the curvature of this worm's x") - private IrisShapedGeneratorStyle xStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, -2, 2); - - @Desc("The style used to determine the curvature of this worm's y") - private IrisShapedGeneratorStyle yStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, -2, 2); - - @Desc("The style used to determine the curvature of this worm's z") - private IrisShapedGeneratorStyle zStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, -2, 2); - - @Desc("The max block distance this worm can travel from its start. This can have performance implications at ranges over 1,000 blocks but it's not too serious, test.") - private int maxDistance = 128; - - @Desc("The iterations this worm can make") - private int maxIterations = 512; - - @Desc("By default if a worm loops back into itself, it stops at that point and does not continue. This is an optimization, to prevent this turn this option on.") - private boolean allowLoops = false; - - @Desc("The thickness of the worms. Each individual worm has the same thickness while traveling however, each spawned worm will vary in thickness.") - private IrisStyledRange girth = new IrisStyledRange().setMin(3).setMax(5) - .setStyle(new IrisGeneratorStyle(NoiseStyle.PERLIN)); - - public KList generate(RNG rng, IrisData data, MantleWriter writer, IrisRange verticalRange, int x, int y, int z, boolean breakSurface, double distance) { - int itr = maxIterations; - double jx, jy, jz; - double cx = x; - double cy = y; - double cz = z; - IrisPosition start = new IrisPosition(x, y, z); - KList pos = new KList<>(); - KSet check = allowLoops ? null : new KSet<>(); - CNG gx = xStyle.getGenerator().create(rng.nextParallelRNG(14567), data); - CNG gy = yStyle.getGenerator().create(rng.nextParallelRNG(64789), data); - CNG gz = zStyle.getGenerator().create(rng.nextParallelRNG(34790), data); - - while (itr-- > 0) { - IrisPosition current = new IrisPosition(Math.round(cx), Math.round(cy), Math.round(cz)); - pos.add(current); - - if (check != null) { - check.add(current); - } - - jx = gx.fitDouble(xStyle.getMin(), xStyle.getMax(), cx, cy, cz); - jy = gy.fitDouble(yStyle.getMin(), yStyle.getMax(), cx, cy, cz); - jz = gz.fitDouble(zStyle.getMin(), zStyle.getMax(), cx, cy, cz); - cx += jx; - cy += jy; - cz += jz; - IrisPosition next = new IrisPosition(Math.round(cx), Math.round(cy), Math.round(cz)); - - if (!breakSurface && writer.getEngineMantle().getHighest(next.getX(), next.getZ(), true) <= next.getY() + distance) { - break; - } - - if (verticalRange != null && !verticalRange.contains(next.getY())) { - break; - } - - if (!writer.isWithin((int) Math.round(cx), verticalRange != null ? (int) Math.round(cy) : 5, (int) Math.round(cz))) { - break; - } - - if (next.isLongerThan(start, maxDistance)) { - break; - } - - if (check != null && check.contains(next)) { - break; - } - } - - return pos; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/TileData.java b/core/src/main/java/art/arcane/iris/engine/object/TileData.java index c2385e849..ba09d9611 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/TileData.java +++ b/core/src/main/java/art/arcane/iris/engine/object/TileData.java @@ -49,7 +49,7 @@ import java.util.Objects; @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TileData implements Cloneable { - private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create(); + private static final Gson gson = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create(); private static final boolean BUKKIT_PRESENT = detectBukkit(); private static volatile TileReader PLATFORM_READER = null; private static volatile TileFactory PLATFORM_FACTORY = null; diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java deleted file mode 100644 index 23a01a9df..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListBiomeDownfallType.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object.annotations; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target({PARAMETER, TYPE, FIELD}) -public @interface RegistryListBiomeDownfallType { - -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ResourceLoadersFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ResourceLoadersFunction.java deleted file mode 100644 index c1e0d212e..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/ResourceLoadersFunction.java +++ /dev/null @@ -1,28 +0,0 @@ -package art.arcane.iris.engine.object.annotations.functions; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.ResourceLoader; -import art.arcane.iris.engine.framework.ListFunction; -import art.arcane.volmlib.util.collection.KList; - -public class ResourceLoadersFunction implements ListFunction> { - @Override - public String key() { - return "resource-loader"; - } - - @Override - public String fancyName() { - return "Resource Loader"; - } - - @Override - public KList apply(IrisData data) { - return data.getLoaders() - .values() - .stream() - .filter(rl -> ResourceLoader.class.equals(rl.getClass())) - .map(ResourceLoader::getFolderName) - .collect(KList.collector()); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java deleted file mode 100644 index febc5e515..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacement.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object.matter; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.IrisEngine; -import art.arcane.iris.engine.object.IRare; -import art.arcane.iris.engine.object.IrisStyledRange; -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.iris.engine.object.annotations.Required; -import art.arcane.iris.engine.object.annotations.Snippet; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.mantle.runtime.Mantle; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterSlice; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("matter-placer") -@EqualsAndHashCode() -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Represents an iris object placer. It places matter objects.") -@Data -public class IrisMatterPlacement implements IRare { - @RegistryListResource(IrisMatterObject.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("List of objects to place") - private KList place = new KList<>(); - - @MinNumber(0) - @Desc("The rarity of this object placing") - private int rarity = 0; - - @MinNumber(0) - @Desc("The styled density of this object") - private IrisStyledRange densityRange; - - @Desc("The absolute density for this object") - private double density = 1; - - @Desc("Translate this matter object before placement") - private IrisMatterTranslate translate; - - @Desc("Place this object on the surface height, bedrock or the sky, then use translate if need be.") - private IrisMatterPlacementLocation location = IrisMatterPlacementLocation.SURFACE; - - public void place(IrisEngine engine, IrisData data, RNG rng, int ax, int az) { - IrisMatterObject object = data.getMatterLoader().load(place.getRandom(rng)); - int x = ax; - int z = az; - int yoff = 0; - - if (translate != null) { - x += translate.xOffset(data, rng, x, z); - yoff += translate.yOffset(data, rng, x, z); - z += translate.zOffset(data, rng, x, z); - } - - int y = yoff + location.at(engine, x, z); - Mantle mantle = engine.getMantle().getMantle(); - - int xx = x; - int yy = y; - int zz = z; - - for (MatterSlice slice : object.getMatter().getSliceMap().values()) { - slice.iterate((mx, my, mz, v) -> { - mantle.set(xx + mx, yy + my, zz + mz, v); - }); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacementLocation.java b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacementLocation.java deleted file mode 100644 index c55eb13e2..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterPlacementLocation.java +++ /dev/null @@ -1,23 +0,0 @@ -package art.arcane.iris.engine.object.matter; - -import art.arcane.iris.engine.IrisEngine; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.volmlib.util.function.Function3; - -@Desc("WHERE THINGS PLACE") -public enum IrisMatterPlacementLocation { - SURFACE((e, x, z) -> e.getHeight(x, z, true)), - SURFACE_ON_FLUID((e, x, z) -> e.getHeight(x, z, false)), - BEDROCK((e, x, z) -> 0), - SKY((e, x, z) -> e.getHeight()); - - private final Function3 computer; - - IrisMatterPlacementLocation(Function3 computer) { - this.computer = computer; - } - - public int at(IrisEngine engine, int x, int z) { - return computer.apply(engine, x, z); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterTranslate.java b/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterTranslate.java deleted file mode 100644 index c3678adb9..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/matter/IrisMatterTranslate.java +++ /dev/null @@ -1,56 +0,0 @@ -package art.arcane.iris.engine.object.matter; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.IrisStyledRange; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.volmlib.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode() -@Accessors(chain = true) -@Desc("Represents a matter translator") -public class IrisMatterTranslate { - @Desc("For varied coordinate shifts use ranges not the literal coordinate") - private IrisStyledRange rangeX = null; - @Desc("For varied coordinate shifts use ranges not the literal coordinate") - private IrisStyledRange rangeY = null; - @Desc("For varied coordinate shifts use ranges not the literal coordinate") - private IrisStyledRange rangeZ = null; - @Desc("Define an absolute shift instead of varied.") - private int x = 0; - @Desc("Define an absolute shift instead of varied.") - private int y = 0; - @Desc("Define an absolute shift instead of varied.") - private int z = 0; - - public int xOffset(IrisData data, RNG rng, int rx, int rz) { - if (rangeX != null) { - return (int) Math.round(rangeX.get(rng, rx, rz, data)); - } - - return x; - } - - public int yOffset(IrisData data, RNG rng, int rx, int rz) { - if (rangeY != null) { - return (int) Math.round(rangeY.get(rng, rx, rz, data)); - } - - return y; - } - - public int zOffset(IrisData data, RNG rng, int rx, int rz) { - if (rangeZ != null) { - return (int) Math.round(rangeZ.get(rng, rx, rz, data)); - } - - return z; - } -} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java index 9e0c82848..50b128673 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java @@ -38,6 +38,7 @@ public final class BukkitBlockState implements PlatformBlockState { private final BlockData data; private final String key; private final String namespace; + private volatile String materialKey; private volatile Boolean air; private volatile Boolean solid; private volatile Boolean occluding; @@ -135,6 +136,17 @@ public final class BukkitBlockState implements PlatformBlockState { return namespace; } + @Override + public String materialKey() { + String cached = materialKey; + if (cached == null) { + int bracket = key.indexOf('['); + cached = bracket < 0 ? key : key.substring(0, bracket); + materialKey = cached; + } + return cached; + } + @Override public boolean isAir() { Boolean cached = air; diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java index fa1a8d705..68ab291e8 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitPlatform.java @@ -43,8 +43,11 @@ import org.bukkit.Registry; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.inventory.ItemStack; @@ -155,6 +158,10 @@ public final class BukkitPlatform implements IrisPlatform { return hudLanes; } + public static void showProgressLane(Player player, String laneId, String title, double progress, long staleMillis) { + hudLanes().show(player, laneId, title, progress, BarColor.BLUE, BarStyle.SOLID, staleMillis); + } + public static void hostConsoleSender(Supplier supplier) { CONSOLE = supplier; } @@ -315,8 +322,8 @@ public final class BukkitPlatform implements IrisPlatform { } @Override - public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) { - if (!(world instanceof World bukkitWorld) || entityKey == null) { + public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) { + if (world == null || entityKey == null || !(world.nativeHandle() instanceof World bukkitWorld)) { return false; } NamespacedKey namespacedKey = NamespacedKey.fromString(entityKey); diff --git a/core/src/main/java/art/arcane/iris/util/common/board/BoardEntry.java b/core/src/main/java/art/arcane/iris/util/common/board/BoardEntry.java deleted file mode 100644 index 8d3b11361..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/board/BoardEntry.java +++ /dev/null @@ -1,21 +0,0 @@ -package art.arcane.iris.util.common.board; - -public class BoardEntry { - private final art.arcane.volmlib.util.board.BoardEntry delegate; - - private BoardEntry(art.arcane.volmlib.util.board.BoardEntry delegate) { - this.delegate = delegate; - } - - public String getPrefix() { - return delegate.getPrefix(); - } - - public String getSuffix() { - return delegate.getSuffix(); - } - - public static BoardEntry translateToEntry(String input) { - return new BoardEntry(art.arcane.volmlib.util.board.BoardEntry.translateToEntry(input)); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/BiomeMap.java b/core/src/main/java/art/arcane/iris/util/common/data/BiomeMap.java deleted file mode 100644 index a8dd10136..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/BiomeMap.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data; - -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; - -public class BiomeMap { - private final IrisBiome[] height; - - public BiomeMap() { - height = new IrisBiome[256]; - } - - public void setBiome(int x, int z, IrisBiome h) { - height[PowerOfTwoCoordinates.packLocal16(x, z)] = h; - } - - public IrisBiome getBiome(int x, int z) { - return height[PowerOfTwoCoordinates.packLocal16(x, z)]; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java b/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java index 4bdc40709..fa07ecce0 100644 --- a/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java +++ b/core/src/main/java/art/arcane/iris/util/common/data/VectorMap.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.Nullable; import java.util.Iterator; import java.util.Map; +import java.util.NoSuchElementException; import java.util.function.BiConsumer; import java.util.function.Function; @@ -23,6 +24,7 @@ public class VectorMap implements Iterable> { } public boolean containsKey(@NonNull IrisBlockVector vector) { + if (map.isEmpty()) return false; var chunk = map.get(chunk(vector)); return chunk != null && chunk.containsKey(relative(vector)); } @@ -32,6 +34,7 @@ public class VectorMap implements Iterable> { } public @Nullable T get(@NonNull IrisBlockVector vector) { + if (map.isEmpty()) return null; var chunk = map.get(chunk(vector)); return chunk == null ? null : chunk.get(relative(vector)); } @@ -46,9 +49,19 @@ public class VectorMap implements Iterable> { .computeIfAbsent(relative(vector), $ -> mappingFunction.apply(vector)); } + @SuppressWarnings("unchecked") public @Nullable T remove(@NonNull IrisBlockVector vector) { - var chunk = map.get(chunk(vector)); - return chunk == null ? null : chunk.remove(relative(vector)); + if (map.isEmpty()) return null; + Key relative = relative(vector); + Object[] removed = new Object[1]; + + // computeIfPresent so the emptied bucket is pruned atomically against a concurrent put. + map.computeIfPresent(chunk(vector), (key, chunk) -> { + removed[0] = chunk.remove(relative); + return chunk.isEmpty() ? null : chunk; + }); + + return (T) removed[0]; } public void putAll(@NonNull VectorMap map) { @@ -85,6 +98,16 @@ public class VectorMap implements Iterable> { return new EntryIterator(); } + /** + * Allocation-free entry walk. {@link EntryIterator} allocates a resolved vector plus a Map.Entry per element; + * the cursor resolves into one reused vector instead. Only valid for callers that do not retain the vector + * returned by {@link Cursor#key()} beyond the current step - clone it if it must outlive the next + * {@link Cursor#next()}. + */ + public @NotNull Cursor cursor() { + return new Cursor(); + } + public @NotNull KeyIterator keys() { return new KeyIterator(); } @@ -93,6 +116,48 @@ public class VectorMap implements Iterable> { return new ValueIterator(); } + public final class Cursor { + private final Iterator>> chunkIterator = map.entrySet().iterator(); + private final IrisBlockVector position = new IrisBlockVector(0, 0, 0); + private Iterator> relativeIterator; + private int rX, rY, rZ; + private T value; + + public boolean next() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) { + value = null; + return false; + } + + Map.Entry> chunk = chunkIterator.next(); + rX = chunk.getKey().x << 10; + rY = chunk.getKey().y << 10; + rZ = chunk.getKey().z << 10; + relativeIterator = chunk.getValue().entrySet().iterator(); + } + + Map.Entry entry = relativeIterator.next(); + Key relative = entry.getKey(); + position.setX(rX + relative.x); + position.setY(rY + relative.y); + position.setZ(rZ + relative.z); + value = entry.getValue(); + return true; + } + + /** + * The position of the current element. The same instance is returned every step. + */ + public @NotNull IrisBlockVector key() { + return position; + } + + public T value() { + return value; + } + } + public class EntryIterator implements Iterator> { private final Iterator>> chunkIterator = map.entrySet().iterator(); private Iterator> relativeIterator; @@ -100,13 +165,20 @@ public class VectorMap implements Iterable> { @Override public boolean hasNext() { - return relativeIterator != null && relativeIterator.hasNext() || chunkIterator.hasNext(); + return advance(); } @Override public Map.Entry next() { - if (relativeIterator == null || !relativeIterator.hasNext()) { - if (!chunkIterator.hasNext()) throw new IllegalStateException("No more elements"); + if (!advance()) throw new NoSuchElementException(); + + var entry = relativeIterator.next(); + return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue()); + } + + private boolean advance() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) return false; var chunk = chunkIterator.next(); rX = chunk.getKey().x << 10; rY = chunk.getKey().y << 10; @@ -114,8 +186,7 @@ public class VectorMap implements Iterable> { relativeIterator = chunk.getValue().entrySet().iterator(); } - var entry = relativeIterator.next(); - return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue()); + return true; } @Override @@ -132,12 +203,19 @@ public class VectorMap implements Iterable> { @Override public boolean hasNext() { - return relativeIterator != null && relativeIterator.hasNext() || chunkIterator.hasNext(); + return advance(); } @Override public IrisBlockVector next() { - if (relativeIterator == null || !relativeIterator.hasNext()) { + if (!advance()) throw new NoSuchElementException(); + + return relativeIterator.next().resolve(rX, rY, rZ); + } + + private boolean advance() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) return false; var chunk = chunkIterator.next(); rX = chunk.getKey().x << 10; rY = chunk.getKey().y << 10; @@ -145,7 +223,7 @@ public class VectorMap implements Iterable> { relativeIterator = chunk.getValue().keySet().iterator(); } - return relativeIterator.next().resolve(rX, rY, rZ); + return true; } @Override @@ -166,15 +244,23 @@ public class VectorMap implements Iterable> { @Override public boolean hasNext() { - return relativeIterator != null && relativeIterator.hasNext() || chunkIterator.hasNext(); + return advance(); } @Override public T next() { - if (relativeIterator == null || !relativeIterator.hasNext()) { + if (!advance()) throw new NoSuchElementException(); + + return relativeIterator.next(); + } + + private boolean advance() { + while (relativeIterator == null || !relativeIterator.hasNext()) { + if (!chunkIterator.hasNext()) return false; relativeIterator = chunkIterator.next().values().iterator(); } - return relativeIterator.next(); + + return true; } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/BitStorage.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/BitStorage.java deleted file mode 100644 index 19ecad3ee..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/BitStorage.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import org.apache.commons.lang3.Validate; - -import java.util.concurrent.atomic.AtomicLongArray; -import java.util.function.IntConsumer; - -public class BitStorage { - private static final int[] MAGIC = new int[]{ - -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, - 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, - 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, - 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, - 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, - 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, - 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, - 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, - 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, - 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, - 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, - 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, - 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, - 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, - 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, - 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, - 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, - 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, - 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5}; - - private final AtomicLongArray data; - private final int bits; - private final long mask; - private final int size; - private final int valuesPerLong; - private final int divideMul; - private final int divideAdd; - private final int divideShift; - - public BitStorage(int bits, int length) { - this(bits, length, (AtomicLongArray) null); - } - - public BitStorage(int bits, int length, long[] data) { - this(bits, length, atomic(data)); - } - - public BitStorage(int bits, int length, AtomicLongArray data) { - Validate.inclusiveBetween(1L, 32L, bits); - this.size = length; - this.bits = bits; - this.mask = (1L << bits) - 1L; - this.valuesPerLong = (char) (64 / bits); - int var3 = 3 * (this.valuesPerLong - 1); - this.divideMul = MAGIC[var3]; - this.divideAdd = MAGIC[var3 + 1]; - this.divideShift = MAGIC[var3 + 2]; - int var4 = (length + this.valuesPerLong - 1) / this.valuesPerLong; - if (data != null) { - if (data.length() != var4) { - throw new RuntimeException("NO!"); - } - this.data = data; - } else { - this.data = new AtomicLongArray(var4); - } - } - - private static AtomicLongArray atomic(long[] data) { - if (data == null) { - return null; - } - - AtomicLongArray d = new AtomicLongArray(data.length); - for (int i = 0; i < data.length; i++) { - d.set(i, data[i]); - } - - return d; - } - - private static long[] atomic(AtomicLongArray data) { - if (data == null) { - return null; - } - - long[] d = new long[data.length()]; - for (int i = 0; i < data.length(); i++) { - d[i] = data.get(i); - } - - return d; - } - - private int cellIndex(int var0) { - long var1 = Integer.toUnsignedLong(this.divideMul); - long var3 = Integer.toUnsignedLong(this.divideAdd); - return (int) (var0 * var1 + var3 >> 32L >> this.divideShift); - } - - public int getAndSet(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data.get(var2); - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - int var6 = (int) (var3 >> var5 & this.mask); - this.data.set(var2, var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5); - return var6; - } - - public void set(int var0, int var1) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - Validate.inclusiveBetween(0L, this.mask, var1); - int var2 = cellIndex(var0); - long var3 = this.data.get(var2); - int var5 = (var0 - var2 * this.valuesPerLong) * this.bits; - - this.data.set(var2, var3 & (this.mask << var5 ^ 0xFFFFFFFFFFFFFFFFL) | (var1 & this.mask) << var5); - } - - public int get(int var0) { - Validate.inclusiveBetween(0L, (this.size - 1), var0); - int var1 = cellIndex(var0); - long var2 = this.data.get(var1); - int var4 = (var0 - var1 * this.valuesPerLong) * this.bits; - return (int) (var2 >> var4 & this.mask); - } - - public long[] getRaw() { - return atomic(data); - } - - public int getSize() { - return this.size; - } - - public int getBits() { - return this.bits; - } - - public void getAll(IntConsumer var0) { - int var1 = 0; - for (int i = 0; i < data.length(); i++) { - long var5 = data.get(i); - for (int var7 = 0; var7 < this.valuesPerLong; var7++) { - var0.accept((int) (var5 & this.mask)); - var5 >>= this.bits; - if (++var1 >= this.size) - return; - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/CrudeIncrementalIntIdentityHashBiMap.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/CrudeIncrementalIntIdentityHashBiMap.java deleted file mode 100644 index 1f19224ee..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/CrudeIncrementalIntIdentityHashBiMap.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import com.google.common.collect.Iterators; - -import java.util.Iterator; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicIntegerArray; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class CrudeIncrementalIntIdentityHashBiMap implements IdMap { - public static final int NOT_FOUND = -1; - private static final Object EMPTY_SLOT = null; - private static final float LOADFACTOR = 0.8F; - private AtomicReferenceArray keys; - private AtomicIntegerArray values; - private AtomicReferenceArray byId; - private int nextId; - private int size; - - public CrudeIncrementalIntIdentityHashBiMap(int var0) { - var0 = (int) (var0 / 0.8F); - this.keys = new AtomicReferenceArray<>(var0); - this.values = new AtomicIntegerArray(var0); - this.byId = new AtomicReferenceArray<>(var0); - } - - public int getId(K var0) { - return getValue(indexOf(var0, hash(var0))); - } - - - public K byId(int var0) { - if (var0 < 0 || var0 >= this.byId.length()) { - return null; - } - return this.byId.get(var0); - } - - private int getValue(int var0) { - if (var0 == -1) { - return -1; - } - return this.values.get(var0); - } - - public boolean contains(K var0) { - return (getId(var0) != -1); - } - - public boolean contains(int var0) { - return (byId(var0) != null); - } - - public int add(K var0) { - int var1 = nextId(); - addMapping(var0, var1); - return var1; - } - - private int nextId() { - while (nextId < byId.length() && byId.get(nextId) != null) { - nextId++; - } - return nextId; - } - - private void grow(int var0) { - AtomicReferenceArray var1 = this.keys; - AtomicIntegerArray var2 = this.values; - this.keys = new AtomicReferenceArray<>(var0); - this.values = new AtomicIntegerArray(var0); - this.byId = new AtomicReferenceArray<>(var0); - this.nextId = 0; - this.size = 0; - for (int var3 = 0; var3 < var1.length(); var3++) { - if (var1.get(var3) != null) { - addMapping(var1.get(var3), var2.get(var3)); - } - } - } - - public void addMapping(K var0, int var1) { - int var2 = Math.max(var1, this.size + 1); - if (var2 >= this.keys.length() * 0.8F) { - int i = this.keys.length() << 1; - while (i < var1) - i <<= 1; - grow(i); - } - int var3 = findEmpty(hash(var0)); - this.keys.set(var3, var0); - this.values.set(var3, var1); - this.byId.set(var1, var0); - this.size++; - if (var1 == this.nextId) - this.nextId++; - } - - private int hash(K var0) { - return (Mth.murmurHash3Mixer(System.identityHashCode(var0)) & Integer.MAX_VALUE) % this.keys.length(); - } - - private int indexOf(K var0, int var1) { - int var2; - for (var2 = var1; var2 < this.keys.length(); var2++) { - if (this.keys.get(var2) == null) { - return 0; - } - if (this.keys.get(var2).equals(var0)) - return var2; - if (this.keys.get(var2) == EMPTY_SLOT) - return -1; - } - for (var2 = 0; var2 < var1; var2++) { - if (this.keys.get(var2).equals(var0)) - return var2; - if (this.keys.get(var2) == EMPTY_SLOT) - return -1; - } - return -1; - } - - private int findEmpty(int var0) { - int var1; - for (var1 = var0; var1 < this.keys.length(); var1++) { - if (this.keys.get(var1) == EMPTY_SLOT) - return var1; - } - for (var1 = 0; var1 < var0; var1++) { - if (this.keys.get(var1) == EMPTY_SLOT) - return var1; - } - throw new RuntimeException("Overflowed :("); - } - - public Iterator iterator() { - return Iterators.filter(new Iterator() { - int i = 0; - - @Override - public boolean hasNext() { - return i < byId.length() - 1; - } - - @Override - public K next() { - return byId.get(i++); - } - }, Objects::nonNull); - } - - public void clear() { - - for (int i = 0; i < Math.max(keys.length(), byId.length()); i++) { - if (i < keys.length() - 1) { - keys.set(i, null); - } - - if (i < byId.length() - 1) { - byId.set(i, null); - } - } - - this.nextId = 0; - this.size = 0; - } - - public int size() { - return this.size; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/GlobalPalette.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/GlobalPalette.java deleted file mode 100644 index 245104923..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/GlobalPalette.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import java.util.List; -import java.util.function.Predicate; - -public class GlobalPalette implements Palette { - private final IdMapper registry; - - private final T defaultValue; - - public GlobalPalette(T... f) { - IdMapper mapper = new IdMapper<>(); - for (T i : f) { - mapper.add(i); - } - registry = mapper; - defaultValue = f[0]; - } - - public GlobalPalette(IdMapper var0, T var1) { - this.registry = var0; - this.defaultValue = var1; - } - - public int idFor(T var0) { - int var1 = this.registry.getId(var0); - return (var1 == -1) ? 0 : var1; - } - - public boolean maybeHas(Predicate var0) { - return true; - } - - public T valueFor(int var0) { - T var1 = this.registry.byId(var0); - return (var1 == null) ? this.defaultValue : var1; - } - - public int getSize() { - return this.registry.size(); - } - - @Override - public void read(List fromList) { - - } - - @Override - public void write(List toList) { - - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/HashMapPalette.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/HashMapPalette.java deleted file mode 100644 index 075180084..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/HashMapPalette.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.util.collection.KMap; - -import java.util.List; - -public class HashMapPalette implements Palette { - private final KMap values; - private final PaletteResize resizeHandler; - private final int bits; - private int id; - - public HashMapPalette(int var1, PaletteResize var2) { - this.bits = var1; - this.resizeHandler = var2; - this.values = new KMap<>(); - id = 1; - } - - public int idFor(T var0) { - if (var0 == null) { - return 0; - } - - return this.values.computeIfAbsent(var0, (k) -> { - int newId = id++; - - if (newId >= 1 << this.bits) { - IrisLogging.info(newId + " to..."); - newId = this.resizeHandler.onResize(this.bits + 1, var0); - IrisLogging.info(newId + ".."); - } - - return newId; - }); - } - - public T valueFor(int var0) { - return this.values.getKey(var0); - } - - public int getSize() { - return this.values.size(); - } - - @Override - public void read(List data) { - data.forEach(this::idFor); - } - - @Override - public void write(List toList) { - toList.addAll(values.keySet()); - } -} \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/IdMap.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/IdMap.java deleted file mode 100644 index 2b763eb3b..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/IdMap.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -public interface IdMap extends Iterable { - int getId(T paramT); - - T byId(int paramInt); -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/IdMapper.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/IdMapper.java deleted file mode 100644 index 69f8d5a30..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/IdMapper.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import com.google.common.base.Predicates; -import com.google.common.collect.Iterators; -import com.google.common.collect.Lists; - -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; - -public class IdMapper implements IdMap { - public static final int DEFAULT = -1; - private final IdentityHashMap tToId; - private final List idToT; - private int nextId; - - public IdMapper(IdentityHashMap tToId, List idToT, int nextId) { - this.tToId = tToId; - this.idToT = idToT; - this.nextId = nextId; - } - - public IdMapper() { - this(512); - } - - public IdMapper(int var0) { - this.idToT = Lists.newArrayListWithExpectedSize(var0); - this.tToId = new IdentityHashMap<>(var0); - } - - public void addMapping(T var0, int var1) { - this.tToId.put(var0, Integer.valueOf(var1)); - while (this.idToT.size() <= var1) { - this.idToT.add(null); - } - this.idToT.set(var1, var0); - if (this.nextId <= var1) - this.nextId = var1 + 1; - } - - public void add(T var0) { - addMapping(var0, this.nextId); - } - - public int getId(T var0) { - Integer var1 = this.tToId.get(var0); - return (var1 == null) ? -1 : var1.intValue(); - } - - public final T byId(int var0) { - if (var0 >= 0 && var0 < this.idToT.size()) { - return this.idToT.get(var0); - } - return null; - } - - public Iterator iterator() { - return Iterators.filter(this.idToT.iterator(), Predicates.notNull()); - } - - public boolean contains(int var0) { - return (byId(var0) != null); - } - - public int size() { - return this.tToId.size(); - } -} \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/LinearPalette.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/LinearPalette.java deleted file mode 100644 index cd87b64e1..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/LinearPalette.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import java.util.List; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public class LinearPalette implements Palette { - private final AtomicReferenceArray values; - private final PaletteResize resizeHandler; - private final int bits; - private int size; - - public LinearPalette(int var1, PaletteResize var2) { - this.values = new AtomicReferenceArray<>(1 << var1); - this.bits = var1; - this.resizeHandler = var2; - } - - public int idFor(T var0) { - int var1; - for (var1 = 0; var1 < size; var1++) { - if (values.get(var1) == null && var0 == null) { - return var1; - } - - if (values.get(var1) != null && values.get(var1).equals(var0)) { - return var1; - } - } - var1 = size; - if (var1 < values.length()) { - values.set(var1, var0); - size++; - return var1; - } - return resizeHandler.onResize(bits + 1, var0); - } - - public T valueFor(int var0) { - if (var0 >= 0 && var0 < size) { - return this.values.get(var0); - } - return null; - } - - public int getSize() { - return size; - } - - @Override - public void read(List fromList) { - for (int i = 0; i < fromList.size(); i++) { - values.set(i, fromList.get(i)); - } - - size = fromList.size(); - } - - @Override - public void write(List toList) { - for (int i = 0; i < size; i++) { - T v = values.get(i); - toList.add(v); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/Mth.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/Mth.java deleted file mode 100644 index 64e535353..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/Mth.java +++ /dev/null @@ -1,710 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import java.util.Random; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.function.Supplier; - -public class Mth { - public static final float PI = 3.1415927F; - public static final float HALF_PI = 1.5707964F; - public static final float TWO_PI = 6.2831855F; - public static final float DEG_TO_RAD = 0.017453292F; - public static final float RAD_TO_DEG = 57.295776F; - public static final float EPSILON = 1.0E-5F; - public static final float SQRT_OF_TWO = sqrt(2.0F); - private static final int BIG_ENOUGH_INT = 1024; - private static final float BIG_ENOUGH_FLOAT = 1024.0F; - private static final long UUID_VERSION = 61440L; - private static final long UUID_VERSION_TYPE_4 = 16384L; - private static final long UUID_VARIANT = -4611686018427387904L; - private static final long UUID_VARIANT_2 = -9223372036854775808L; - private static final float SIN_SCALE = 10430.378F; - - private static final float[] SIN; - private static final Random RANDOM = new Random(); - private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[]{ - 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, - 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, - 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, - 10, 9}; - private static final double ONE_SIXTH = 0.16666666666666666D; - private static final int FRAC_EXP = 8; - private static final int LUT_SIZE = 257; - private static final double FRAC_BIAS = Double.longBitsToDouble(4805340802404319232L); - private static final double[] ASIN_TAB = new double[257]; - private static final double[] COS_TAB = new double[257]; - - static { - SIN = make(new float[65536], var0 -> { - for (int var1 = 0; var1 < var0.length; var1++) - var0[var1] = (float) Math.sin(var1 * Math.PI * 2.0D / 65536.0D); - }); - } - - static { - for (int var0 = 0; var0 < 257; var0++) { - double var1 = var0 / 256.0D; - double var3 = Math.asin(var1); - COS_TAB[var0] = Math.cos(var3); - ASIN_TAB[var0] = var3; - } - } - - public static T make(Supplier var0) { - return var0.get(); - } - - public static T make(T var0, Consumer var1) { - var1.accept(var0); - return var0; - } - - public static float sin(float var0) { - return SIN[(int) (var0 * 10430.378F) & 0xFFFF]; - } - - public static float cos(float var0) { - return SIN[(int) (var0 * 10430.378F + 16384.0F) & 0xFFFF]; - } - - public static float sqrt(float var0) { - return (float) Math.sqrt(var0); - } - - public static int floor(float var0) { - int var1 = (int) var0; - return (var0 < var1) ? (var1 - 1) : var1; - } - - public static int fastFloor(double var0) { - return (int) (var0 + 1024.0D) - 1024; - } - - public static int floor(double var0) { - int var2 = (int) var0; - return (var0 < var2) ? (var2 - 1) : var2; - } - - public static long lfloor(double var0) { - long var2 = (long) var0; - return (var0 < var2) ? (var2 - 1L) : var2; - } - - public static int absFloor(double var0) { - return (int) ((var0 >= 0.0D) ? var0 : (-var0 + 1.0D)); - } - - public static float abs(float var0) { - return Math.abs(var0); - } - - public static int abs(int var0) { - return Math.abs(var0); - } - - public static int ceil(float var0) { - int var1 = (int) var0; - return (var0 > var1) ? (var1 + 1) : var1; - } - - public static int ceil(double var0) { - int var2 = (int) var0; - return (var0 > var2) ? (var2 + 1) : var2; - } - - public static byte clamp(byte var0, byte var1, byte var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static int clamp(int var0, int var1, int var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static long clamp(long var0, long var2, long var4) { - if (var0 < var2) - return var2; - if (var0 > var4) - return var4; - return var0; - } - - public static float clamp(float var0, float var1, float var2) { - if (var0 < var1) - return var1; - if (var0 > var2) - return var2; - return var0; - } - - public static double clamp(double var0, double var2, double var4) { - if (var0 < var2) - return var2; - if (var0 > var4) - return var4; - return var0; - } - - public static double clampedLerp(double var0, double var2, double var4) { - if (var4 < 0.0D) - return var0; - if (var4 > 1.0D) - return var2; - return lerp(var4, var0, var2); - } - - public static float clampedLerp(float var0, float var1, float var2) { - if (var2 < 0.0F) - return var0; - if (var2 > 1.0F) - return var1; - return lerp(var2, var0, var1); - } - - public static double absMax(double var0, double var2) { - if (var0 < 0.0D) - var0 = -var0; - if (var2 < 0.0D) - var2 = -var2; - return (var0 > var2) ? var0 : var2; - } - - public static int intFloorDiv(int var0, int var1) { - return Math.floorDiv(var0, var1); - } - - public static int nextInt(Random var0, int var1, int var2) { - if (var1 >= var2) - return var1; - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float nextFloat(Random var0, float var1, float var2) { - if (var1 >= var2) - return var1; - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static double nextDouble(Random var0, double var1, double var3) { - if (var1 >= var3) - return var1; - return var0.nextDouble() * (var3 - var1) + var1; - } - - public static double average(long[] var0) { - long var1 = 0L; - for (long var6 : var0) - var1 += var6; - return var1 / var0.length; - } - - public static boolean equal(float var0, float var1) { - return (Math.abs(var1 - var0) < 1.0E-5F); - } - - public static boolean equal(double var0, double var2) { - return (Math.abs(var2 - var0) < 9.999999747378752E-6D); - } - - public static int positiveModulo(int var0, int var1) { - return Math.floorMod(var0, var1); - } - - public static float positiveModulo(float var0, float var1) { - return (var0 % var1 + var1) % var1; - } - - public static double positiveModulo(double var0, double var2) { - return (var0 % var2 + var2) % var2; - } - - public static int wrapDegrees(int var0) { - int var1 = var0 % 360; - if (var1 >= 180) - var1 -= 360; - if (var1 < -180) - var1 += 360; - return var1; - } - - public static float wrapDegrees(float var0) { - float var1 = var0 % 360.0F; - if (var1 >= 180.0F) - var1 -= 360.0F; - if (var1 < -180.0F) - var1 += 360.0F; - return var1; - } - - public static double wrapDegrees(double var0) { - double var2 = var0 % 360.0D; - if (var2 >= 180.0D) - var2 -= 360.0D; - if (var2 < -180.0D) - var2 += 360.0D; - return var2; - } - - public static float degreesDifference(float var0, float var1) { - return wrapDegrees(var1 - var0); - } - - public static float degreesDifferenceAbs(float var0, float var1) { - return abs(degreesDifference(var0, var1)); - } - - public static float rotateIfNecessary(float var0, float var1, float var2) { - float var3 = degreesDifference(var0, var1); - float var4 = clamp(var3, -var2, var2); - return var1 - var4; - } - - public static float approach(float var0, float var1, float var2) { - var2 = abs(var2); - if (var0 < var1) - return clamp(var0 + var2, var0, var1); - return clamp(var0 - var2, var1, var0); - } - - public static float approachDegrees(float var0, float var1, float var2) { - float var3 = degreesDifference(var0, var1); - return approach(var0, var0 + var3, var2); - } - - public static int getInt(String var0, int var1) { - return Integer.valueOf(var0, var1); - } - - public static int getInt(String var0, int var1, int var2) { - return Math.max(var2, getInt(var0, var1)); - } - - public static double getDouble(String var0, double var1) { - try { - return Double.parseDouble(var0); - } catch (Throwable var3) { - return var1; - } - } - - public static double getDouble(String var0, double var1, double var3) { - return Math.max(var3, getDouble(var0, var1)); - } - - public static int smallestEncompassingPowerOfTwo(int var0) { - int var1 = var0 - 1; - var1 |= var1 >> 1; - var1 |= var1 >> 2; - var1 |= var1 >> 4; - var1 |= var1 >> 8; - var1 |= var1 >> 16; - return var1 + 1; - } - - public static boolean isPowerOfTwo(int var0) { - return (var0 != 0 && (var0 & var0 - 1) == 0); - } - - public static int ceillog2(int var0) { - var0 = isPowerOfTwo(var0) ? var0 : smallestEncompassingPowerOfTwo(var0); - return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int) (var0 * 125613361L >> 27L) & 0x1F]; - } - - public static int log2(int var0) { - return ceillog2(var0) - (isPowerOfTwo(var0) ? 0 : 1); - } - - public static int color(float var0, float var1, float var2) { - return color(floor(var0 * 255.0F), floor(var1 * 255.0F), floor(var2 * 255.0F)); - } - - public static int color(int var0, int var1, int var2) { - int var3 = var0; - var3 = (var3 << 8) + var1; - var3 = (var3 << 8) + var2; - return var3; - } - - public static int colorMultiply(int var0, int var1) { - int var2 = (var0 & 0xFF0000) >> 16; - int var3 = (var1 & 0xFF0000) >> 16; - int var4 = (var0 & 0xFF00) >> 8; - int var5 = (var1 & 0xFF00) >> 8; - int var6 = (var0 & 0xFF); - int var7 = (var1 & 0xFF); - int var8 = (int) (var2 * var3 / 255.0F); - int var9 = (int) (var4 * var5 / 255.0F); - int var10 = (int) (var6 * var7 / 255.0F); - return var0 & 0xFF000000 | var8 << 16 | var9 << 8 | var10; - } - - public static int colorMultiply(int var0, float var1, float var2, float var3) { - int var4 = (var0 & 0xFF0000) >> 16; - int var5 = (var0 & 0xFF00) >> 8; - int var6 = (var0 & 0xFF); - int var7 = (int) (var4 * var1); - int var8 = (int) (var5 * var2); - int var9 = (int) (var6 * var3); - return var0 & 0xFF000000 | var7 << 16 | var8 << 8 | var9; - } - - public static float frac(float var0) { - return var0 - floor(var0); - } - - public static double frac(double var0) { - return var0 - lfloor(var0); - } - - public static long getSeed(int var0, int var1, int var2) { - long var3 = (var0 * 3129871) ^ var2 * 116129781L ^ var1; - var3 = var3 * var3 * 42317861L + var3 * 11L; - return var3 >> 16L; - } - - public static UUID createInsecureUUID(Random var0) { - long var1 = var0.nextLong() & 0xFFFFFFFFFFFF0FFFL | 0x4000L; - long var3 = var0.nextLong() & 0x3FFFFFFFFFFFFFFFL | Long.MIN_VALUE; - return new UUID(var1, var3); - } - - public static UUID createInsecureUUID() { - return createInsecureUUID(RANDOM); - } - - public static double inverseLerp(double var0, double var2, double var4) { - return (var0 - var2) / (var4 - var2); - } - - public static double atan2(double var0, double var2) { - double var4 = var2 * var2 + var0 * var0; - if (Double.isNaN(var4)) - return Double.NaN; - boolean var6 = (var0 < 0.0D); - if (var6) - var0 = -var0; - boolean var7 = (var2 < 0.0D); - if (var7) - var2 = -var2; - boolean var8 = (var0 > var2); - if (var8) { - double d = var2; - var2 = var0; - var0 = d; - } - double var9 = fastInvSqrt(var4); - var2 *= var9; - var0 *= var9; - double var11 = FRAC_BIAS + var0; - int var13 = (int) Double.doubleToRawLongBits(var11); - double var14 = ASIN_TAB[var13]; - double var16 = COS_TAB[var13]; - double var18 = var11 - FRAC_BIAS; - double var20 = var0 * var16 - var2 * var18; - double var22 = (6.0D + var20 * var20) * var20 * 0.16666666666666666D; - double var24 = var14 + var22; - if (var8) - var24 = 1.5707963267948966D - var24; - if (var7) - var24 = Math.PI - var24; - if (var6) - var24 = -var24; - return var24; - } - - public static float fastInvSqrt(float var0) { - float var1 = 0.5F * var0; - int var2 = Float.floatToIntBits(var0); - var2 = 1597463007 - (var2 >> 1); - var0 = Float.intBitsToFloat(var2); - var0 *= 1.5F - var1 * var0 * var0; - return var0; - } - - public static double fastInvSqrt(double var0) { - double var2 = 0.5D * var0; - long var4 = Double.doubleToRawLongBits(var0); - var4 = 6910469410427058090L - (var4 >> 1L); - var0 = Double.longBitsToDouble(var4); - var0 *= 1.5D - var2 * var0 * var0; - return var0; - } - - public static float fastInvCubeRoot(float var0) { - int var1 = Float.floatToIntBits(var0); - var1 = 1419967116 - var1 / 3; - float var2 = Float.intBitsToFloat(var1); - var2 = 0.6666667F * var2 + 1.0F / 3.0F * var2 * var2 * var0; - var2 = 0.6666667F * var2 + 1.0F / 3.0F * var2 * var2 * var0; - return var2; - } - - public static int hsvToRgb(float var0, float var1, float var2) { - float var8, var9, var10; - int var11, var12, var13, var3 = (int) (var0 * 6.0F) % 6; - float var4 = var0 * 6.0F - var3; - float var5 = var2 * (1.0F - var1); - float var6 = var2 * (1.0F - var4 * var1); - float var7 = var2 * (1.0F - (1.0F - var4) * var1); - switch (var3) { - case 0: - var8 = var2; - var9 = var7; - var10 = var5; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 1: - var8 = var6; - var9 = var2; - var10 = var5; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 2: - var8 = var5; - var9 = var2; - var10 = var7; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 3: - var8 = var5; - var9 = var6; - var10 = var2; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 4: - var8 = var7; - var9 = var5; - var10 = var2; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - case 5: - var8 = var2; - var9 = var5; - var10 = var6; - var11 = clamp((int) (var8 * 255.0F), 0, 255); - var12 = clamp((int) (var9 * 255.0F), 0, 255); - var13 = clamp((int) (var10 * 255.0F), 0, 255); - return var11 << 16 | var12 << 8 | var13; - } - throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + var0 + ", " + var1 + ", " + var2); - } - - public static int murmurHash3Mixer(int var0) { - var0 ^= var0 >>> 16; - var0 *= -2048144789; - var0 ^= var0 >>> 13; - var0 *= -1028477387; - var0 ^= var0 >>> 16; - return var0; - } - - public static long murmurHash3Mixer(long var0) { - var0 ^= var0 >>> 33L; - var0 *= -49064778989728563L; - var0 ^= var0 >>> 33L; - var0 *= -4265267296055464877L; - var0 ^= var0 >>> 33L; - return var0; - } - - public static double[] cumulativeSum(double... var0) { - float var1 = 0.0F; - for (double var5 : var0) - var1 = (float) (var1 + var5); - int var2; - for (var2 = 0; var2 < var0.length; var2++) - var0[var2] = var0[var2] / var1; - for (var2 = 0; var2 < var0.length; var2++) - var0[var2] = ((var2 == 0) ? 0.0D : var0[var2 - 1]) + var0[var2]; - return var0; - } - - public static int getRandomForDistributionIntegral(Random var0, double[] var1) { - double var2 = var0.nextDouble(); - for (int var4 = 0; var4 < var1.length; var4++) { - if (var2 < var1[var4]) - return var4; - } - return var1.length; - } - - public static double[] binNormalDistribution(double var0, double var2, double var4, int var6, int var7) { - double[] var8 = new double[var7 - var6 + 1]; - int var9 = 0; - for (int var10 = var6; var10 <= var7; var10++) { - var8[var9] = Math.max(0.0D, var0 * - - StrictMath.exp(-(var10 - var4) * (var10 - var4) / 2.0D * var2 * var2)); - var9++; - } - return var8; - } - - public static double[] binBiModalNormalDistribution(double var0, double var2, double var4, double var6, double var8, double var10, int var12, int var13) { - double[] var14 = new double[var13 - var12 + 1]; - int var15 = 0; - for (int var16 = var12; var16 <= var13; var16++) { - var14[var15] = Math.max(0.0D, var0 * - - StrictMath.exp(-(var16 - var4) * (var16 - var4) / 2.0D * var2 * var2) + var6 * - StrictMath.exp(-(var16 - var10) * (var16 - var10) / 2.0D * var8 * var8)); - var15++; - } - return var14; - } - - public static double[] binLogDistribution(double var0, double var2, int var4, int var5) { - double[] var6 = new double[var5 - var4 + 1]; - int var7 = 0; - for (int var8 = var4; var8 <= var5; var8++) { - var6[var7] = Math.max(var0 * StrictMath.log(var8) + var2, 0.0D); - var7++; - } - return var6; - } - - public static float lerp(float var0, float var1, float var2) { - return var1 + var0 * (var2 - var1); - } - - public static double lerp(double var0, double var2, double var4) { - return var2 + var0 * (var4 - var2); - } - - public static double lerp2(double var0, double var2, double var4, double var6, double var8, double var10) { - return lerp(var2, - - lerp(var0, var4, var6), - lerp(var0, var8, var10)); - } - - public static double lerp3(double var0, double var2, double var4, double var6, double var8, double var10, double var12, double var14, double var16, double var18, double var20) { - return lerp(var4, - - lerp2(var0, var2, var6, var8, var10, var12), - lerp2(var0, var2, var14, var16, var18, var20)); - } - - public static double smoothstep(double var0) { - return var0 * var0 * var0 * (var0 * (var0 * 6.0D - 15.0D) + 10.0D); - } - - public static double smoothstepDerivative(double var0) { - return 30.0D * var0 * var0 * (var0 - 1.0D) * (var0 - 1.0D); - } - - public static int sign(double var0) { - if (var0 == 0.0D) - return 0; - return (var0 > 0.0D) ? 1 : -1; - } - - public static float rotLerp(float var0, float var1, float var2) { - return var1 + var0 * wrapDegrees(var2 - var1); - } - - public static float diffuseLight(float var0, float var1, float var2) { - return Math.min(var0 * var0 * 0.6F + var1 * var1 * (3.0F + var1) / 4.0F + var2 * var2 * 0.8F, 1.0F); - } - - public static float rotlerp(float var0, float var1, float var2) { - float var3 = var1 - var0; - while (var3 < -180.0F) - var3 += 360.0F; - while (var3 >= 180.0F) - var3 -= 360.0F; - return var0 + var2 * var3; - } - - public static float rotWrap(double var0) { - while (var0 >= 180.0D) - var0 -= 360.0D; - while (var0 < -180.0D) - var0 += 360.0D; - return (float) var0; - } - - public static float triangleWave(float var0, float var1) { - return (Math.abs(var0 % var1 - var1 * 0.5F) - var1 * 0.25F) / var1 * 0.25F; - } - - public static float square(float var0) { - return var0 * var0; - } - - public static double square(double var0) { - return var0 * var0; - } - - public static int square(int var0) { - return var0 * var0; - } - - public static double clampedMap(double var0, double var2, double var4, double var6, double var8) { - return clampedLerp(var6, var8, inverseLerp(var0, var2, var4)); - } - - public static double map(double var0, double var2, double var4, double var6, double var8) { - return lerp(inverseLerp(var0, var2, var4), var6, var8); - } - - public static double wobble(double var0) { - return var0 + (2.0D * (new Random(floor(var0 * 3000.0D))).nextDouble() - 1.0D) * 1.0E-7D / 2.0D; - } - - public static int roundToward(int var0, int var1) { - return (var0 + var1 - 1) / var1 * var1; - } - - public static int randomBetweenInclusive(Random var0, int var1, int var2) { - return var0.nextInt(var2 - var1 + 1) + var1; - } - - public static float randomBetween(Random var0, float var1, float var2) { - return var0.nextFloat() * (var2 - var1) + var1; - } - - public static float normal(Random var0, float var1, float var2) { - return var1 + (float) var0.nextGaussian() * var2; - } - - public static double length(int var0, double var1, int var3) { - return Math.sqrt((var0 * var0) + var1 * var1 + (var3 * var3)); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/Palette.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/Palette.java deleted file mode 100644 index 48ce56920..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/Palette.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import java.util.List; - -public interface Palette { - int idFor(T paramT); - - T valueFor(int paramInt); - - int getSize(); - - void read(List fromList); - - void write(List toList); -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteAccess.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteAccess.java deleted file mode 100644 index 926138267..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteAccess.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import art.arcane.volmlib.util.nbt.tag.CompoundTag; - -public interface PaletteAccess { - void setBlock(int x, int y, int z, CompoundTag data); - - CompoundTag getBlock(int x, int y, int z); - - void writeToSection(CompoundTag tag); - - void readFromSection(CompoundTag tag); -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteResize.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteResize.java deleted file mode 100644 index 5c72c26fb..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteResize.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -interface PaletteResize { - int onResize(int paramInt, T paramT); -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteType.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteType.java deleted file mode 100644 index edea162e2..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/PaletteType.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import art.arcane.volmlib.util.data.Varint; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public interface PaletteType { - void writePaletteNode(DataOutputStream dos, T t) throws IOException; - - T readPaletteNode(DataInputStream din) throws IOException; - - default void writeList(DataOutputStream dos, List list) throws IOException { - Varint.writeUnsignedVarInt(list.size(), dos); - for (T i : list) { - writePaletteNode(dos, i); - } - } - - default List readList(DataInputStream din) throws IOException { - int v = Varint.readUnsignedVarInt(din); - List t = new ArrayList<>(); - - for (int i = 0; i < v; i++) { - t.add(readPaletteNode(din)); - } - - return t; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/PalettedContainer.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/PalettedContainer.java deleted file mode 100644 index c9c9431ad..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/PalettedContainer.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.util.math.M; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; - -import java.util.List; - -public class PalettedContainer implements PaletteResize { - public static final int GLOBAL_PALETTE_BITS = 9; - public static final int MIN_PALETTE_SIZE = 4; - private static final int SIZE = 4096; - private final PaletteResize dummyPaletteResize = (var0, var1) -> 0; - protected BitStorage storage; - private Palette palette; - private int bits; - - public PalettedContainer() { - setBits(4); - } - - private static int getIndex(int var0, int var1, int var2) { - return var1 << 8 | var2 << 4 | var0; - } - - private void setBits(int var0) { - if (var0 == this.bits) { - return; - } - this.bits = var0; - if (this.bits <= 4) { - this.bits = 4; - this.palette = new LinearPalette<>(this.bits, this); - } else { - this.palette = new HashMapPalette<>(this.bits, this); - } - - this.palette.idFor(null); - this.storage = new BitStorage(this.bits, 4096); - } - - public int onResize(int var0, T var1) { - BitStorage var2 = this.storage; - Palette var3 = this.palette; - setBits(var0); - for (int var4 = 0; var4 < var2.getSize(); var4++) { - T var5 = var3.valueFor(var2.get(var4)); - if (var5 != null) { - set(var4, var5); - } - } - - return this.palette.idFor(var1); - } - - public T getAndSet(int var0, int var1, int var2, T var3) { - return getAndSet(getIndex(var0, var1, var2), var3); - } - - public T getAndSetUnchecked(int var0, int var1, int var2, T var3) { - return getAndSet(getIndex(var0, var1, var2), var3); - } - - private T getAndSet(int var0, T var1) { - int var2 = this.palette.idFor(var1); - int var3 = this.storage.getAndSet(var0, var2); - return this.palette.valueFor(var3); - } - - public void set(int var0, int var1, int var2, T var3) { - set(getIndex(var0, var1, var2), var3); - } - - private void set(int var0, T var1) { - int var2 = this.palette.idFor(var1); - - if (M.r(0.003)) { - IrisLogging.info("ID for " + var1 + " is " + var2 + " Palette: " + palette.getSize()); - } - - this.storage.set(var0, var2); - } - - public T get(int var0, int var1, int var2) { - return get(getIndex(var0, var1, var2)); - } - - protected T get(int var0) { - return this.palette.valueFor(this.storage.get(var0)); - } - - public void read(List palette, long[] data) { - int var2 = Math.max(4, Mth.ceillog2(palette.size())); - if (var2 != this.bits) { - setBits(var2); - } - - this.palette.read(palette); - int var3 = data.length * 64 / 4096; - if (var3 == this.bits) { - System.arraycopy(data, 0, this.storage.getRaw(), 0, data.length); - } else { - BitStorage var4 = new BitStorage(var3, 4096, data); - for (int var5 = 0; var5 < 4096; var5++) { - this.storage.set(var5, var4.get(var5)); - } - } - } - - public long[] write(List toList) { - HashMapPalette var3 = new HashMapPalette<>(this.bits, this.dummyPaletteResize); - T var4 = null; - int var5 = 0; - int[] var6 = new int[4096]; - for (int i = 0; i < 4096; i++) { - T t = get(i); - if (t != var4) { - var4 = t; - var5 = var3.idFor(t); - } - var6[i] = var5; - } - - var3.write(toList); - int var8 = Math.max(4, Mth.ceillog2(toList.size())); - BitStorage var9 = new BitStorage(var8, 4096); - for (int var10 = 0; var10 < var6.length; var10++) { - var9.set(var10, var6[var10]); - } - return var9.getRaw(); - } - - public void count(CountConsumer var0) { - Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); - this.storage.getAll(var1 -> int2IntOpenHashMap.put(var1, int2IntOpenHashMap.get(var1) + 1)); - int2IntOpenHashMap.int2IntEntrySet().forEach(var1 -> var0.accept(this.palette.valueFor(var1.getIntKey()), var1.getIntValue())); - } -} \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/QuartPos.java b/core/src/main/java/art/arcane/iris/util/common/data/palette/QuartPos.java deleted file mode 100644 index d84a3f140..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/QuartPos.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.data.palette; - -public final class QuartPos { - public static final int BITS = 2; - - public static final int SIZE = 4; - - private static final int SECTION_TO_QUARTS_BITS = 2; - - public static int fromBlock(int var0) { - return var0 >> 2; - } - - public static int toBlock(int var0) { - return var0 << 2; - } - - public static int fromSection(int var0) { - return var0 << 2; - } - - public static int toSection(int var0) { - return var0 >> 2; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/DummyHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/DummyHandler.java deleted file mode 100644 index d1edce376..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/DummyHandler.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.util.common.director.specialhandlers; - -import art.arcane.volmlib.util.director.handlers.base.DummyHandlerBase; -import art.arcane.volmlib.util.director.DirectorParameterHandler; - -public class DummyHandler extends DummyHandlerBase implements DirectorParameterHandler { -} diff --git a/core/src/main/java/art/arcane/iris/util/common/inventorygui/ElementEvent.java b/core/src/main/java/art/arcane/iris/util/common/inventorygui/ElementEvent.java deleted file mode 100644 index c64753b95..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/inventorygui/ElementEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package art.arcane.iris.util.common.inventorygui; - -public enum ElementEvent { - LEFT, - RIGHT, - SHIFT_LEFT, - SHIFT_RIGHT, - DRAG_INTO, - OTHER_DRAG_INTO; - - public art.arcane.volmlib.util.inventorygui.ElementEvent toShared() { - return art.arcane.volmlib.util.inventorygui.ElementEvent.valueOf(name()); - } - - public static ElementEvent fromShared(art.arcane.volmlib.util.inventorygui.ElementEvent event) { - return valueOf(event.name()); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/inventorygui/UIVoidDecorator.java b/core/src/main/java/art/arcane/iris/util/common/inventorygui/UIVoidDecorator.java deleted file mode 100644 index 409f90c40..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/inventorygui/UIVoidDecorator.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.inventorygui; - -import art.arcane.volmlib.util.inventorygui.WindowDecorator; - -import art.arcane.volmlib.util.inventorygui.Element; - -public class UIVoidDecorator implements WindowDecorator { - @Override - public Element onDecorateBackground(art.arcane.volmlib.util.inventorygui.Window window, int position, int row) { - return null; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java b/core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java index 99dfdc727..a17c01b37 100644 --- a/core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java +++ b/core/src/main/java/art/arcane/iris/util/common/math/AxisAlignedBB.java @@ -58,14 +58,6 @@ public class AxisAlignedBB { return new AxisAlignedBB(min().add(new IrisPosition((int) x, (int) y, (int) z)), max().add(new IrisPosition((int) x, (int) y, (int) z))); } - public boolean contains(AlignedPoint p) { - return p.getX() >= xa && p.getX() <= xb && p.getY() >= ya && p.getZ() <= yb && p.getZ() >= za && p.getZ() <= zb; - } - - public boolean contains(IrisPosition p) { - return p.getX() >= xa && p.getX() <= xb && p.getY() >= ya && p.getZ() <= yb && p.getZ() >= za && p.getZ() <= zb; - } - public boolean intersects(AxisAlignedBB s) { return this.xb >= s.xa && this.yb >= s.ya && this.zb >= s.za && s.xb >= this.xa && s.yb >= this.ya && s.zb >= this.za; } diff --git a/core/src/main/java/art/arcane/iris/util/common/math/RNGV2.java b/core/src/main/java/art/arcane/iris/util/common/math/RNGV2.java deleted file mode 100644 index 9af9958e5..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/math/RNGV2.java +++ /dev/null @@ -1,167 +0,0 @@ -package art.arcane.iris.util.common.math; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.UUID; - -import java.security.SecureRandom; -import java.util.List; -import java.util.UUID; -import java.nio.charset.StandardCharsets; - -public class RNGV2 extends SecureRandom { - private static final long serialVersionUID = 5222938581174415179L; - private static final char[] CHARGEN = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=!@#$%^&*()_+`~[];',./<>?:\\\"{}|\\\\".toCharArray(); - private final long sx; - - // Constructor with no seed - public RNGV2() { - super(); - sx = 0; - } - - public RNGV2(long seed) { - super(); - this.setSeed(seed); - this.sx = seed; - } - - // Constructor with a string seed - public RNGV2(String seed) { - this(UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).getLeastSignificantBits() + - UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).getMostSignificantBits() + - (seed.length() * 32564L)); - } - - public RNGV2 nextParallelRNG(int signature) { - return new RNGV2(sx + signature); - } - - public RNGV2 nextParallelRNG(long signature) { - return new RNGV2(sx + signature); - } - - public String s(int length) { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < length; i++) { - sb.append(c()); - } - - return sb.toString(); - } - - public char c() { - return CHARGEN[i(CHARGEN.length - 1)]; - } - - // Pick a random enum - public T e(Class t) { - T[] c = t.getEnumConstants(); - return c[i(c.length)]; - } - - public boolean b() { - return nextBoolean(); - } - - public boolean b(double percent) { - return d() > percent; - } - - public short si(int lowerBound, int upperBound) { - return (short) (lowerBound + (nextFloat() * ((upperBound - lowerBound) + 1))); - } - - public short si(int upperBound) { - return si(0, upperBound); - } - - public short si() { - return si(1); - } - - public float f(float lowerBound, float upperBound) { - return lowerBound + (nextFloat() * ((upperBound - lowerBound))); - } - - public float f(float upperBound) { - return f(0, upperBound); - } - - public float f() { - return f(1); - } - - public double d(double lowerBound, double upperBound) { - return lowerBound + (nextDouble() * (upperBound - lowerBound)); - } - - public double d(double upperBound) { - return d(0, upperBound); - } - - public double d() { - return nextDouble(); - } - - public int i(int lowerBound, int upperBound) { - if (lowerBound >= upperBound) { - throw new IllegalArgumentException("Upper bound must be greater than lower bound"); - } - return lowerBound + this.nextInt(upperBound - lowerBound + 1); - } - - public int i(int upperBound) { - return i(0, upperBound); - } - - public long l(long lowerBound, long upperBound) { - return Math.round(d(lowerBound, upperBound)); - } - - public long l(long upperBound) { - return l(0, upperBound); - } - - public int imax() { - return i(Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - public long lmax() { - return l(Long.MIN_VALUE, Long.MAX_VALUE); - } - - public float fmax() { - return f(Float.MIN_VALUE, Float.MAX_VALUE); - } - - public double dmax() { - return d(Double.MIN_VALUE, Double.MAX_VALUE); - } - - public short simax() { - return si(Short.MIN_VALUE, Short.MAX_VALUE); - } - - public boolean chance(double chance) { - return nextDouble() <= chance; - } - - public T pick(List pieces) { - if (pieces.isEmpty()) { - return null; - } - - if (pieces.size() == 1) { - return pieces.get(0); - } - - return pieces.get(this.nextInt(pieces.size())); - } - - public long getSeed() { - return sx; - } -} - diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/BurstedHunk.java b/core/src/main/java/art/arcane/iris/util/common/parallel/BurstedHunk.java deleted file mode 100644 index 58bd1df95..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/parallel/BurstedHunk.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.parallel; - - -import art.arcane.iris.util.project.hunk.Hunk; -public interface BurstedHunk extends Hunk, art.arcane.volmlib.util.parallel.BurstedHunk { -} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/NOOPGridLock.java b/core/src/main/java/art/arcane/iris/util/common/parallel/NOOPGridLock.java deleted file mode 100644 index 09ca012eb..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/parallel/NOOPGridLock.java +++ /dev/null @@ -1,9 +0,0 @@ -package art.arcane.iris.util.common.parallel; - -import art.arcane.volmlib.util.parallel.NoopGridLockSupport; - -public class NOOPGridLock extends NoopGridLockSupport { - public NOOPGridLock(int x, int z) { - super(x, z); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/StreamUtils.java b/core/src/main/java/art/arcane/iris/util/common/parallel/StreamUtils.java deleted file mode 100644 index 8649fd628..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/parallel/StreamUtils.java +++ /dev/null @@ -1,29 +0,0 @@ -package art.arcane.iris.util.common.parallel; - -import art.arcane.volmlib.util.parallel.StreamUtilsSupport; -import art.arcane.volmlib.util.math.Position2; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; - -public class StreamUtils { - - public static Stream streamRadius(int x, int z, int radius) { - return streamRadius(x, z, radius, radius); - } - - public static Stream streamRadius(int x, int z, int radiusX, int radiusZ) { - return StreamUtilsSupport.streamRadius(x, z, radiusX, radiusZ) - .map(p -> new Position2(p.getX(), p.getZ())); - } - - public static void forEach(Stream stream, Function> mapper, Consumer consumer, @Nullable MultiBurst burst) { - StreamUtilsSupport.forEach(stream, mapper, consumer, burst == null ? null : burst::burst); - } - - public static void forEach(Stream stream, Consumer task, @Nullable MultiBurst burst) { - StreamUtilsSupport.forEach(stream, task, burst == null ? null : burst::burst); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/parallel/SyncExecutor.java b/core/src/main/java/art/arcane/iris/util/common/parallel/SyncExecutor.java deleted file mode 100644 index fb9722c38..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/parallel/SyncExecutor.java +++ /dev/null @@ -1,20 +0,0 @@ -package art.arcane.iris.util.common.parallel; - -import art.arcane.volmlib.util.parallel.SyncExecutorSupport; -import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.scheduling.SR; - -public class SyncExecutor extends SyncExecutorSupport { - public SyncExecutor(int msPerTick) { - super(msPerTick, M::ms, task -> { - SR sr = new SR() { - @Override - public void run() { - task.run(); - } - }; - - return sr::cancel; - }); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/Command.java b/core/src/main/java/art/arcane/iris/util/common/plugin/Command.java deleted file mode 100644 index b1449c709..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/Command.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Command { - String value() default ""; - -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java b/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java deleted file mode 100644 index a0fe84bb7..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/CommandDummy.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import net.kyori.adventure.text.Component; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionAttachment; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Set; -import java.util.UUID; - -public class CommandDummy implements CommandSender { - @Override - public void sendMessage(@NotNull String message) { - - } - - @Override - public void sendMessage(@NotNull String... messages) { - - } - - @Override - public void sendMessage(@Nullable UUID sender, @NotNull String message) { - - } - - @Override - public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { - - } - - @NotNull - @Override - public Server getServer() { - return null; - } - - @NotNull - @Override - public String getName() { - return null; - } - - @NotNull - @Override - public Component name() { - return Component.empty(); - } - - @NotNull - @Override - public Spigot spigot() { - return null; - } - - @Override - public boolean isPermissionSet(@NotNull String name) { - return false; - } - - @Override - public boolean isPermissionSet(@NotNull Permission perm) { - return false; - } - - @Override - public boolean hasPermission(@NotNull String name) { - return false; - } - - @Override - public boolean hasPermission(@NotNull Permission perm) { - return false; - } - - @NotNull - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { - return null; - } - - @NotNull - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin) { - return null; - } - - @Nullable - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { - return null; - } - - @Nullable - @Override - public PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { - return null; - } - - @Override - public void removeAttachment(@NotNull PermissionAttachment attachment) { - - } - - @Override - public void recalculatePermissions() { - - } - - @NotNull - @Override - public Set getEffectivePermissions() { - return null; - } - - @Override - public boolean isOp() { - return false; - } - - @Override - public void setOp(boolean value) { - - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/Control.java b/core/src/main/java/art/arcane/iris/util/common/plugin/Control.java deleted file mode 100644 index 4a0170a82..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/Control.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Control { - -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/Controller.java b/core/src/main/java/art/arcane/iris/util/common/plugin/Controller.java deleted file mode 100644 index 1a89ff8ec..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/Controller.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.iris.spi.IrisLogging; - -public abstract class Controller implements IController { - private final String name; - private int tickRate; - - public Controller() { - name = getClass().getSimpleName().replaceAll("Controller", "") + " Controller"; - tickRate = -1; - } - - protected void setTickRate(@SuppressWarnings("SameParameterValue") int rate) { - this.tickRate = rate; - } - - protected void disableTicking() { - setTickRate(-1); - } - - @Override - public void l(Object l) { - IrisLogging.info("[" + getName() + "]: " + l); - } - - @Override - public void w(Object l) { - IrisLogging.warn("[" + getName() + "]: " + l); - } - - @Override - public void f(Object l) { - IrisLogging.error("[" + getName() + "]: " + l); - } - - @Override - public void v(Object l) { - IrisLogging.debug("[" + getName() + "]: " + l); - } - - @Override - public String getName() { - return name; - } - - @Override - public abstract void start(); - - @Override - public abstract void stop(); - - @Override - public abstract void tick(); - - @Override - public int getTickInterval() { - return tickRate; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/ICommand.java b/core/src/main/java/art/arcane/iris/util/common/plugin/ICommand.java deleted file mode 100644 index aa231e69b..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/ICommand.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.volmlib.util.collection.KList; - -/** - * Represents a pawn command - * - * @author cyberpwn - */ -public interface ICommand { - KList getRequiredPermissions(); - - /** - * Get the name of this command (node) - * - * @return the node - */ - String getNode(); - - /** - * Get all (realized) nodes of this command - * - * @return the nodes - */ - KList getNodes(); - - /** - * Get all (every) node in this command - * - * @return all nodes - */ - KList getAllNodes(); - - /** - * Add a node to this command - * - * @param node the node - */ - void addNode(String node); - - /** - * Handle a command. If this is a subcommand, parameters after the subcommand - * will be adapted in args for you - * - * @param sender the volume sender (pre-tagged) - * @param args the arguments after this command node - * @return return true to mark it as handled - */ - boolean handle(VolmitSender sender, String[] args); - - KList handleTab(VolmitSender sender, String[] args); -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/IController.java b/core/src/main/java/art/arcane/iris/util/common/plugin/IController.java deleted file mode 100644 index 38245d3a8..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/IController.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import org.bukkit.event.Listener; - -@SuppressWarnings("EmptyMethod") -public interface IController extends Listener { - String getName(); - - void start(); - - void stop(); - - void tick(); - - int getTickInterval(); - - void l(Object l); - - void w(Object l); - - void f(Object l); - - void v(Object l); -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/Instance.java b/core/src/main/java/art/arcane/iris/util/common/plugin/Instance.java deleted file mode 100644 index da2da9699..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/Instance.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Instance { - -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/MortarCommand.java b/core/src/main/java/art/arcane/iris/util/common/plugin/MortarCommand.java deleted file mode 100644 index f51307c2d..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/MortarCommand.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.core.IrisSettings; -import art.arcane.volmlib.util.collection.KList; -import org.bukkit.Sound; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.util.Comparator; - -import art.arcane.iris.core.localization.BukkitRuntimeMessages; -import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.volmlib.util.localization.MessageArgument; -/** - * Represents a pawn command - * - * @author cyberpwn - */ -public abstract class MortarCommand implements ICommand { - private final KList children; - private final KList nodes; - private final KList requiredPermissions; - private final String node; - private String category; - private String description; - - /** - * Override this with a super constructor as most commands shouldn't change - * these parameters - * - * @param node the node (primary node) i.e. volume - * @param nodes the aliases. i.e. v, vol, bile - */ - public MortarCommand(String node, String... nodes) { - category = ""; - this.node = node; - this.nodes = new KList<>(nodes); - requiredPermissions = new KList<>(); - children = buildChildren(); - description = "No Description"; - } - - @Override - public KList handleTab(VolmitSender sender, String[] args) { - KList v = new KList<>(); - if (args.length == 0) { - for (MortarCommand i : getChildren()) { - v.add(i.getNode()); - } - } - - addTabOptions(sender, args, v); - - if (v.isEmpty()) { - return null; - } - - if (sender.isPlayer() && IrisSettings.get().getGeneral().isCommandSounds()) { - sender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 0.25f, 1.7f); - } - - return v; - } - - public abstract void addTabOptions(VolmitSender sender, String[] args, KList list); - - protected abstract String getArgsUsage(); - - public String getDescription() { - return description; - } - - protected void setDescription(String description) { - this.description = description; - } - - protected void requiresPermission(MortarPermission node) { - if (node == null) { - return; - } - - requiresPermission(node.toString()); - } - - protected void requiresPermission(String node) { - if (node == null) { - return; - } - - requiredPermissions.add(node); - } - - public void rejectAny(int past, VolmitSender sender, String[] a) { - if (a.length > past) { - int p = past; - - StringBuilder m = new StringBuilder(); - - for (String i : a) { - p--; - if (p < 0) { - m.append(i).append(", "); - } - } - - if (!m.toString().trim().isEmpty()) { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.MORTAR_COMMAND_PARAMETERS_IGNORED, MessageArgument.untrusted("m", String.valueOf(m)))); - } - } - } - - @Override - public String getNode() { - return node; - } - - @Override - public KList getNodes() { - return nodes; - } - - @Override - public KList getAllNodes() { - return getNodes().copy().qadd(getNode()); - } - - @Override - public void addNode(String node) { - getNodes().add(node); - } - - public KList getChildren() { - return children; - } - - private KList buildChildren() { - KList p = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Command.class)) { - try { - i.setAccessible(true); - MortarCommand pc = (MortarCommand) i.getType().getConstructor().newInstance(); - Command c = i.getAnnotation(Command.class); - - if (!c.value().trim().isEmpty()) { - pc.setCategory(c.value().trim()); - } else { - pc.setCategory(getCategory()); - } - - p.add(pc); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - } - - p.sort(Comparator.comparing(MortarCommand::getNode)); - - return p; - } - - @Override - public KList getRequiredPermissions() { - return requiredPermissions; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/MortarPermission.java b/core/src/main/java/art/arcane/iris/util/common/plugin/MortarPermission.java deleted file mode 100644 index 2931925e1..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/MortarPermission.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.util.collection.KList; -import org.bukkit.command.CommandSender; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; - -public abstract class MortarPermission { - private MortarPermission parent; - - public MortarPermission() { - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - MortarPermission px = (MortarPermission) i.getType().getConstructor().newInstance(); - px.setParent(this); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, px); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - } - } - - public KList getChildren() { - KList p = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - p.add((MortarPermission) i.get(Modifier.isStatic(i.getModifiers()) ? null : this)); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - } - - return p; - } - - public String getFullNode() { - if (hasParent()) { - return getParent().getFullNode() + "." + getNode(); - } - - return getNode(); - } - - protected abstract String getNode(); - - public abstract String getDescription(); - - public abstract boolean isDefault(); - - @Override - public String toString() { - return getFullNode(); - } - - public boolean hasParent() { - return getParent() != null; - } - - public MortarPermission getParent() { - return parent; - } - - public void setParent(MortarPermission parent) { - this.parent = parent; - } - - public boolean has(CommandSender sender) { - return sender.hasPermission(getFullNode()); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/Permission.java b/core/src/main/java/art/arcane/iris/util/common/plugin/Permission.java deleted file mode 100644 index b10f9dde8..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/Permission.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Retention(RUNTIME) -@Target(FIELD) -public @interface Permission { - -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistry.java b/core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistry.java deleted file mode 100644 index a568883e0..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistry.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class PluginRegistry { - private final KMap registry = new KMap<>(); - @Getter - private final String namespace; - - public void unregisterAll() { - registry.clear(); - } - - public KList getRegistries() { - return registry.k(); - } - - public T get(String s) { - if (!registry.containsKey(s)) { - return null; - } - - return registry.get(s); - } - - public void register(String s, T t) { - registry.put(s, t); - } - - public void unregister(String s) { - registry.remove(s); - } - - public T resolve(String id) { - if (registry.isEmpty()) { - return null; - } - - return registry.get(id); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistryGroup.java b/core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistryGroup.java deleted file mode 100644 index 20b7f01e1..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/PluginRegistryGroup.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; - -public class PluginRegistryGroup { - private final KMap> registries = new KMap<>(); - - public T resolve(String namespace, String id) { - if (registries.isEmpty()) { - return null; - } - - PluginRegistry r = registries.get(namespace); - if (r == null) { - return null; - } - - return r.resolve(id); - } - - public void clearRegistries() { - registries.clear(); - } - - public void removeRegistry(String namespace) { - registries.remove(namespace); - } - - public PluginRegistry getRegistry(String namespace) { - return registries.computeIfAbsent(namespace, PluginRegistry::new); - } - - public KList compile() { - KList l = new KList<>(); - registries.values().forEach((i) - -> i.getRegistries().forEach((j) - -> l.add(i.getNamespace() + ":" + j))); - return l; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/RouterCommand.java b/core/src/main/java/art/arcane/iris/util/common/plugin/RouterCommand.java deleted file mode 100644 index 7560176f4..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/RouterCommand.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; - -/** - * Assistive command router - * - * @author cyberpwn - */ -public class RouterCommand extends org.bukkit.command.Command { - private final CommandExecutor ex; - private String usage; - - /** - * The router command routes commands to bukkit executors - * - * @param realCommand the real command - * @param ex the executor - */ - public RouterCommand(ICommand realCommand, CommandExecutor ex) { - super(realCommand.getNode().toLowerCase()); - setAliases(realCommand.getNodes()); - - this.ex = ex; - } - - - @Override - public Command setUsage(String u) { - this.usage = u; - return this; - } - - - @Override - public String getUsage() { - return usage; - } - - @Override - public boolean execute(CommandSender sender, String commandLabel, String[] args) { - return ex.onCommand(sender, this, commandLabel, args); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java deleted file mode 100644 index 6d1c7a55d..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VirtualCommand.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.plugin; - -import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.core.IrisSettings; -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.common.scheduling.J; -import art.arcane.volmlib.util.reflect.V; -import org.bukkit.Sound; -import org.bukkit.command.CommandSender; - -import java.lang.reflect.Field; - -import art.arcane.iris.core.localization.BukkitRuntimeMessages; -import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.volmlib.util.localization.MessageArgument; -/** - * Represents a virtual command. A chain of iterative processing through - * subcommands. - * - * @author cyberpwn - */ -public class VirtualCommand { - private final ICommand command; - private final String tag; - - private final KMap, VirtualCommand> children; - - public VirtualCommand(ICommand command) { - this(command, ""); - } - - public VirtualCommand(ICommand command, String tag) { - this.command = command; - children = new KMap<>(); - this.tag = tag; - - for (Field i : command.getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Command.class)) { - try { - Command cc = i.getAnnotation(Command.class); - ICommand cmd = (ICommand) i.getType().getConstructor().newInstance(); - new V(command, true, true).set(i.getName(), cmd); - children.put(cmd.getAllNodes(), new VirtualCommand(cmd, cc.value().trim().isEmpty() ? tag : cc.value().trim())); - } catch (Exception e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - } - } - - public String getTag() { - return tag; - } - - public ICommand getCommand() { - return command; - } - - public KMap, VirtualCommand> getChildren() { - return children; - } - - public boolean hit(CommandSender sender, KList chain) { - return hit(sender, chain, null); - } - - public boolean hit(CommandSender sender, KList chain, String label) { - VolmitSender vs = new VolmitSender(sender); - vs.setTag(tag); - - if (label != null) { - vs.setCommand(label); - } - - if (chain.isEmpty()) { - if (!checkPermissions(sender, command)) { - return true; - } - - return command.handle(vs, new String[0]); - } - - String nl = chain.get(0); - - for (KList i : children.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(nl)) { - vs.setCommand(chain.get(0)); - VirtualCommand cmd = children.get(i); - KList c = chain.copy(); - c.remove(0); - if (cmd.hit(sender, c, vs.getCommand())) { - if (vs.isPlayer() && IrisSettings.get().getGeneral().isCommandSounds()) { - vs.player().getWorld().playSound(vs.player().getLocation(), Sound.ITEM_AXE_STRIP, 0.35f, 1.8f); - } - - return true; - } - } - } - } - - if (!checkPermissions(sender, command)) { - return true; - } - - return command.handle(vs, chain.toArray(new String[0])); - } - - public KList hitTab(CommandSender sender, KList chain, String label) { - VolmitSender vs = new VolmitSender(sender); - vs.setTag(tag); - - if (label != null) - vs.setCommand(label); - - if (chain.isEmpty()) { - if (!checkPermissions(sender, command)) { - return null; - } - - return command.handleTab(vs, new String[0]); - } - - String nl = chain.get(0); - - for (KList i : children.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(nl)) { - vs.setCommand(chain.get(0)); - VirtualCommand cmd = children.get(i); - KList c = chain.copy(); - c.remove(0); - KList v = cmd.hitTab(sender, c, vs.getCommand()); - if (v != null) { - return v; - } - } - } - } - - if (!checkPermissions(sender, command)) { - return null; - } - - return command.handleTab(vs, chain.toArray(new String[0])); - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private boolean checkPermissions(CommandSender sender, ICommand command2) { - boolean failed = false; - - for (String i : command.getRequiredPermissions()) { - if (!sender.hasPermission(i)) { - failed = true; - J.s(() -> sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.VIRTUAL_COMMAND_MESSAGE, MessageArgument.untrusted("i", String.valueOf(i)))), 0); - } - } - - if (failed) { - sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.VIRTUAL_COMMAND_INSUFFICIENT_PERMISSIONS)); - return false; - } - - return true; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java index e5ae9f477..c3e74dc88 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitPlugin.java @@ -20,40 +20,22 @@ package art.arcane.iris.util.common.plugin; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.io.IO; -import art.arcane.volmlib.util.math.M; -import art.arcane.volmlib.util.reflect.V; import art.arcane.iris.util.common.scheduling.J; import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandMap; -import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.SimpleCommandMap; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; -import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.util.Iterator; -import java.util.List; -import java.util.Map; @SuppressWarnings("EmptyMethod") public abstract class VolmitPlugin extends JavaPlugin implements Listener { public static final boolean bad = false; private final KList postShutdown = new KList<>(); - private KMap, VirtualCommand> commands; - private KList commandCache; - private KList permissionCache; public File getJarFile() { return getFile(); @@ -84,9 +66,6 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { } public void onEnable() { - registerInstance(); - registerPermissions(); - registerCommands(); J.a(this::outputInfo); registerListener(this); start(); @@ -94,9 +73,6 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { public void unregisterAll() { unregisterListeners(); - unregisterCommands(); - unregisterPermissions(); - unregisterInstance(); } private void outputInfo() { @@ -104,61 +80,12 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { IO.delete(getDataFolder("info")); getDataFolder("info").mkdirs(); outputPluginInfo(); - outputCommandInfo(); - outputPermissionInfo(); } catch (Throwable e) { IrisLogging.reportError(e); } } - private void outputPermissionInfo() throws IOException { - FileConfiguration fc = new YamlConfiguration(); - - for (MortarPermission i : permissionCache) { - chain(i, fc); - } - - fc.save(getDataFile("info", "permissions.yml")); - } - - private void chain(MortarPermission i, FileConfiguration fc) { - KList ff = new KList<>(); - - for (MortarPermission j : i.getChildren()) { - ff.add(j.getFullNode()); - } - - fc.set(i.getFullNode().replaceAll("\\Q.\\E", ",") + "." + "description", i.getDescription()); - fc.set(i.getFullNode().replaceAll("\\Q.\\E", ",") + "." + "default", i.isDefault()); - fc.set(i.getFullNode().replaceAll("\\Q.\\E", ",") + "." + "children", ff); - - for (MortarPermission j : i.getChildren()) { - chain(j, fc); - } - } - - private void outputCommandInfo() throws IOException { - FileConfiguration fc = new YamlConfiguration(); - - for (MortarCommand i : commandCache) { - chain(i, "/", fc); - } - - fc.save(getDataFile("info", "commands.yml")); - } - - private void chain(MortarCommand i, String c, FileConfiguration fc) { - String n = c + (c.length() == 1 ? "" : " ") + i.getNode(); - fc.set(n + "." + "description", i.getDescription()); - fc.set(n + "." + "required-permissions", i.getRequiredPermissions()); - fc.set(n + "." + "aliases", i.getAllNodes()); - - for (MortarCommand j : i.getChildren()) { - chain(j, n, fc); - } - } - private void outputPluginInfo() throws IOException { FileConfiguration fc = new YamlConfiguration(); fc.set("version", getDescription().getVersion()); @@ -166,94 +93,6 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { fc.save(getDataFile("info", "plugin.yml")); } - private void registerPermissions() { - permissionCache = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - i.setAccessible(true); - MortarPermission pc = (MortarPermission) i.getType().getConstructor().newInstance(); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, pc); - registerPermission(pc); - permissionCache.add(pc); - v("Registered Permissions " + pc.getFullNode() + " (" + i.getName() + ")"); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - IrisLogging.reportError(e); - w("Failed to register permission (field " + i.getName() + ")"); - e.printStackTrace(); - } - } - } - - for (org.bukkit.permissions.Permission i : computePermissions()) { - try { - Bukkit.getPluginManager().addPermission(i); - } catch (Throwable e) { - IrisLogging.reportError(e); - - } - } - } - - private KList computePermissions() { - KList g = new KList<>(); - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Permission.class)) { - try { - MortarPermission x = (MortarPermission) i.get(Modifier.isStatic(i.getModifiers()) ? null : this); - g.add(toPermission(x)); - g.addAll(computePermissions(x)); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - IrisLogging.reportError(e); - e.printStackTrace(); - } - } - } - - return g.removeDuplicates(); - } - - private KList computePermissions(MortarPermission p) { - KList g = new KList<>(); - - if (p == null) { - return g; - } - - for (MortarPermission i : p.getChildren()) { - if (i == null) { - continue; - } - - g.add(toPermission(i)); - g.addAll(computePermissions(i)); - } - - return g; - } - - private org.bukkit.permissions.Permission toPermission(MortarPermission p) { - if (p == null) { - return null; - } - - org.bukkit.permissions.Permission perm = new org.bukkit.permissions.Permission(p.getFullNode() + (p.hasParent() ? "" : ".*")); - perm.setDescription(p.getDescription() == null ? "" : p.getDescription()); - perm.setDefault(p.isDefault() ? PermissionDefault.TRUE : PermissionDefault.OP); - - for (MortarPermission i : p.getChildren()) { - perm.getChildren().put(i.getFullNode(), true); - } - - return perm; - } - - private void registerPermission(MortarPermission pc) { - - } - @Override public void onDisable() { stop(); @@ -262,205 +101,6 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { unregisterAll(); } - private void tickController(IController i) { - if (bad) { - return; - } - - if (i.getTickInterval() < 0) { - return; - } - - M.tick++; - if (M.interval(i.getTickInterval())) { - try { - i.tick(); - } catch (Throwable e) { - w("Failed to tick controller " + i.getName()); - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - } - - private void registerInstance() { - if (bad) { - return; - } - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Instance.class)) { - try { - i.setAccessible(true); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, this); - v("Registered Instance " + i.getName()); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - w("Failed to register instance (field " + i.getName() + ")"); - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - } - } - - private void unregisterInstance() { - if (bad) { - return; - } - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(Instance.class)) { - try { - i.setAccessible(true); - i.set(Modifier.isStatic(i.getModifiers()) ? null : this, null); - v("Unregistered Instance " + i.getName()); - } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { - w("Failed to unregister instance (field " + i.getName() + ")"); - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - } - } - - private void registerCommands() { - if (bad) { - return; - } - commands = new KMap<>(); - commandCache = new KList<>(); - - for (Field i : getClass().getDeclaredFields()) { - if (i.isAnnotationPresent(art.arcane.iris.util.common.plugin.Command.class)) { - try { - i.setAccessible(true); - MortarCommand pc = (MortarCommand) i.getType().getConstructor().newInstance(); - art.arcane.iris.util.common.plugin.Command c = i.getAnnotation(art.arcane.iris.util.common.plugin.Command.class); - registerCommand(pc, c.value()); - commandCache.add(pc); - v("Registered Commands /" + pc.getNode() + " (" + i.getName() + ")"); - } catch (IllegalArgumentException | IllegalAccessException | InstantiationException | - InvocationTargetException | NoSuchMethodException | SecurityException e) { - w("Failed to register command (field " + i.getName() + ")"); - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - } - } - - - @Override - public List onTabComplete(CommandSender sender, Command command, - String alias, String[] args) { - if (commands == null || commands.isEmpty()) { - return super.onTabComplete(sender, command, alias, args); - } - - KList chain = new KList<>(); - - for (String i : args) { - if (i.trim().isEmpty()) { - continue; - } - - chain.add(i.trim()); - } - - for (KList i : commands.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(alias)) { - VirtualCommand cmd = commands.get(i); - - List v = cmd.hitTab(sender, chain.copy(), alias); - if (v != null) { - return v; - } - } - } - } - - return super.onTabComplete(sender, command, alias, args); - } - - @Override - public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String label, String[] args) { - if (bad) { - return false; - } - if (commands == null || commands.isEmpty()) { - return false; - } - - KList chain = new KList<>(); - chain.add(args); - - for (KList i : commands.k()) { - for (String j : i) { - if (j.equalsIgnoreCase(label)) { - VirtualCommand cmd = commands.get(i); - - if (cmd.hit(sender, chain.copy(), label)) { - return true; - } - } - } - } - - return false; - } - - public void registerCommand(ICommand cmd) { - registerCommand(cmd, ""); - } - - public void registerCommand(ICommand cmd, String subTag) { - if (bad) { - return; - } - - commands.put(cmd.getAllNodes(), new VirtualCommand(cmd, subTag.trim().isEmpty() ? getTag() : getTag(subTag.trim()))); - PluginCommand cc = getCommand(cmd.getNode().toLowerCase()); - - if (cc != null) { - cc.setExecutor(this); - cc.setUsage(getName() + ":" + getClass().toString() + ":" + cmd.getNode()); - } else { - RouterCommand r = new RouterCommand(cmd, this); - r.setUsage(getName() + ":" + getClass().toString()); - ((CommandMap) new V(Bukkit.getServer()).get("commandMap")).register("", r); - } - } - - public void unregisterCommand(ICommand cmd) { - if (bad) { - return; - } - try { - SimpleCommandMap m = new V(Bukkit.getServer()).get("commandMap"); - - Map k = new V(m).get("knownCommands"); - - for (Iterator> it = k.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if (entry.getValue() instanceof Command) { - org.bukkit.command.Command c = entry.getValue(); - String u = c.getUsage(); - - if (u != null && u.equals(getName() + ":" + getClass().toString() + ":" + cmd.getNode())) { - if (c.unregister(m)) { - it.remove(); - v("Unregistered Command /" + cmd.getNode()); - } else { - Bukkit.getConsoleSender().sendMessage(getTag() + "Failed to unregister command " + c.getName()); - } - } - } - } - } catch (Throwable e) { - e.printStackTrace(); - IrisLogging.reportError(e); - } - } - public String getTag() { if (bad) { return ""; @@ -485,40 +125,6 @@ public abstract class VolmitPlugin extends JavaPlugin implements Listener { HandlerList.unregisterAll((Listener) this); } - public void unregisterCommands() { - if (bad) { - return; - } - if (commands == null || commands.isEmpty()) { - return; - } - for (VirtualCommand i : commands.v()) { - try { - unregisterCommand(i.getCommand()); - } catch (Throwable e) { - IrisLogging.reportError(e); - - } - } - } - - private void unregisterPermissions() { - if (bad) { - return; - } - for (org.bukkit.permissions.Permission i : computePermissions()) { - if (i == null) { - continue; - } - try { - Bukkit.getPluginManager().removePermission(i); - v("Unregistered Permission " + i.getName()); - } catch (Throwable e) { - IrisLogging.reportError(e); - } - } - } - public File getDataFile(String... strings) { File f = new File(getDataFolder(), new KList<>(strings).toString(File.separator)); f.getParentFile().mkdirs(); diff --git a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java index d50244c4d..ddd1e5da8 100644 --- a/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java +++ b/core/src/main/java/art/arcane/iris/util/common/plugin/VolmitSender.java @@ -341,10 +341,6 @@ public class VolmitSender implements CommandSender { @Override public void sendMessage(String message) { - if (s instanceof CommandDummy) { - return; - } - if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { s.sendMessage(C.translateAlternateColorCodes('&', getTag() + message)); return; @@ -363,10 +359,6 @@ public class VolmitSender implements CommandSender { } public void sendMessageRaw(String message) { - if (s instanceof CommandDummy) { - return; - } - if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { s.sendMessage(C.translateAlternateColorCodes('&', message)); return; @@ -381,9 +373,6 @@ public class VolmitSender implements CommandSender { } public void sendComponent(Component component) { - if (s instanceof CommandDummy) { - return; - } deliver(component); } diff --git a/core/src/main/java/art/arcane/iris/util/common/reflect/OldEnum.java b/core/src/main/java/art/arcane/iris/util/common/reflect/OldEnum.java deleted file mode 100644 index 23631b824..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/reflect/OldEnum.java +++ /dev/null @@ -1,99 +0,0 @@ -package art.arcane.iris.util.common.reflect; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.Objects; - -public class OldEnum { - - private static final Class oldEnum; - private static final MethodHandle name; - - public static boolean exists() { - return oldEnum != null; - } - - public static boolean isOldEnum(Class c) { - return oldEnum != null && oldEnum.isAssignableFrom(c); - } - - public static T valueOf(Class c, String name) { - return valueOf(c, name, name.replace(".", "_")); - } - - public static T valueOf(Class c, String... names) { - for (final String name : names) { - try { - return (T) c.getDeclaredField(name).get(null); - } catch (Throwable ignored) {} - } - return null; - } - - public static String name(Object o) { - try { - return (String) name.invoke(o); - } catch (Throwable e) { - return null; - } - } - - public static String[] values(Class clazz) { - if (!isOldEnum(clazz)) return new String[0]; - return Arrays.stream(clazz.getDeclaredFields()) - .filter(f -> Modifier.isStatic(f.getModifiers())) - .filter(f -> Modifier.isFinal(f.getModifiers())) - .map(f -> { - try { - return name(f.get(null)); - } catch (Throwable ignored) { - return null; - } - }) - .filter(Objects::nonNull) - .toArray(String[]::new); - } - - public static TypeAdapter create(Class type) { - if (!isOldEnum(type)) - return null; - - return new TypeAdapter<>() { - - @Override - public void write(JsonWriter out, T value) throws IOException { - out.value(name(value)); - } - - @Override - public T read(JsonReader in) throws IOException { - return valueOf(type, in.nextString()); - } - }; - } - - static { - Class clazz = null; - MethodHandle method = null; - try { - clazz = Class.forName("org.bukkit.util.OldEnum"); - method = MethodHandles.lookup().findVirtual(clazz, "name", MethodType.methodType(String.class)); - } catch (Throwable ignored) {} - - if (clazz == null || method == null) { - oldEnum = null; - name = null; - } else { - oldEnum = clazz; - name = method; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java index 9bcc834d3..e8b99e8fc 100644 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java @@ -425,61 +425,96 @@ public class J { } } - public static CompletableFuture sfut(Runnable r) { - CompletableFuture f = new CompletableFuture(); + /** + * Never returns null; the future always completes, exceptionally if the task throws or cannot be scheduled. + */ + public static CompletableFuture sfut(Runnable r) { + CompletableFuture f = new CompletableFuture<>(); if (!canSchedule()) { - return null; + f.completeExceptionally(new IllegalStateException("Cannot schedule sync task, no scheduler available.")); + return f; } - s(() -> { - r.run(); - f.complete(null); - }); + try { + s(() -> settle(f, r)); + } catch (Throwable e) { + f.completeExceptionally(e); + } return f; } + /** + * Never returns null; the future always completes, exceptionally if the supplier throws or cannot be scheduled. + */ public static CompletableFuture sfut(Supplier r) { CompletableFuture f = new CompletableFuture<>(); if (!canSchedule()) { - return null; + f.completeExceptionally(new IllegalStateException("Cannot schedule sync task, no scheduler available.")); + return f; } - s(() -> { - try { - f.complete(r.get()); - } catch (Throwable e) { - f.completeExceptionally(e); - } - }); + try { + s(() -> settleSupplied(f, r)); + } catch (Throwable e) { + f.completeExceptionally(e); + } return f; } - public static CompletableFuture sfut(Runnable r, int delay) { - CompletableFuture f = new CompletableFuture(); + /** + * Never returns null; the future always completes, exceptionally if the task throws or cannot be scheduled. + */ + public static CompletableFuture sfut(Runnable r, int delay) { + CompletableFuture f = new CompletableFuture<>(); if (!canSchedule()) { - return null; + f.completeExceptionally(new IllegalStateException("Cannot schedule delayed sync task, no scheduler available.")); + return f; } - s(() -> { - r.run(); - f.complete(null); - }, delay); + try { + s(() -> settle(f, r), delay); + } catch (Throwable e) { + f.completeExceptionally(e); + } return f; } - public static CompletableFuture afut(Runnable r) { - CompletableFuture f = new CompletableFuture(); - J.a(() -> { + /** + * Never returns null; the future always completes, exceptionally if the task throws or cannot be scheduled. + */ + public static CompletableFuture afut(Runnable r) { + CompletableFuture f = new CompletableFuture<>(); + + try { + J.a(() -> settle(f, r)); + } catch (Throwable e) { + f.completeExceptionally(e); + } + + return f; + } + + private static void settle(CompletableFuture f, Runnable r) { + try { r.run(); f.complete(null); - }); - return f; + } catch (Throwable e) { + f.completeExceptionally(e); + } + } + + private static void settleSupplied(CompletableFuture f, Supplier r) { + try { + f.complete(r.get()); + } catch (Throwable e) { + f.completeExceptionally(e); + } } public static void s(Runnable r, int delay) { diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java index 4defbd728..763e2fb8d 100644 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/Job.java @@ -31,8 +31,6 @@ import art.arcane.volmlib.util.localization.MessageArgument; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -94,7 +92,7 @@ public interface Job { HudSurface barSurface = barClaim.granted(); sender.sendProgress(getProgress(), getName(), titleSurface, barSurface); if (barSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), BarColor.BLUE, BarStyle.SOLID, 4000L); + BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L); } else if (barSurface == HudSurface.ACTION_BAR) { BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); } diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelRadiusJob.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelRadiusJob.java deleted file mode 100644 index 4a9784d82..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/jobs/ParallelRadiusJob.java +++ /dev/null @@ -1,86 +0,0 @@ -package art.arcane.iris.util.common.scheduling.jobs; - -import art.arcane.volmlib.util.math.Spiraler; -import art.arcane.iris.util.common.parallel.MultiBurst; -import lombok.SneakyThrows; -import lombok.Synchronized; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; - -public abstract class ParallelRadiusJob implements Job { - private final ExecutorService service; - private final AtomicInteger completed; - private volatile int radiusX, radiusZ; - private volatile int offsetX, offsetZ; - private volatile int total; - private final Semaphore lock; - private final int lockSize; - - public ParallelRadiusJob(int concurrent) { - this(concurrent, MultiBurst.burst); - } - - public ParallelRadiusJob(int concurrent, ExecutorService service) { - this.service = service; - completed = new AtomicInteger(0); - lock = new Semaphore(concurrent); - lockSize = concurrent; - } - - public ParallelRadiusJob retarget(int radius, int offsetX, int offsetZ) { - return retarget(radius, radius, offsetX, offsetZ); - } - - @Synchronized - public ParallelRadiusJob retarget(int radiusX, int radiusZ, int offsetX, int offsetZ) { - completed.set(0); - this.radiusX = radiusX; - this.radiusZ = radiusZ; - this.offsetX = offsetX; - this.offsetZ = offsetZ; - total = (radiusX * 2 + 1) * (radiusZ * 2 + 1); - return this; - } - - @Override - @SneakyThrows - @Synchronized - public void execute() { - new Spiraler(radiusX * 2 + 3, radiusZ * 2 + 3, this::submit).drain(); - lock.acquire(lockSize); - lock.release(lockSize); - } - - @SneakyThrows - private void submit(int x, int z) { - if (Math.abs(x) > radiusX || Math.abs(z) > radiusZ) return; - lock.acquire(); - service.submit(() -> { - try { - execute(x + offsetX, z + offsetZ); - } finally { - completeWork(); - } - }); - } - - protected abstract void execute(int x, int z); - - @Override - public void completeWork() { - completed.incrementAndGet(); - lock.release(); - } - - @Override - public int getTotalWork() { - return total; - } - - @Override - public int getWorkCompleted() { - return completed.get(); - } -} 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 721e14119..66dd864ee 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 @@ -105,11 +105,7 @@ public class ChunkContext { return false; } - return prefillAsyncEligible(Thread.currentThread().getName()); - } - - static boolean prefillAsyncEligible(String threadName) { - return threadName != null && threadName.startsWith("Iris "); + return !MultiBurst.burst.ownsCurrentThread(); } public int getX() { diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java index 496efaa43..925240adf 100644 --- a/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/Hunk.java @@ -49,6 +49,7 @@ import art.arcane.iris.util.project.hunk.view.ChunkHunkView; import art.arcane.iris.util.project.hunk.view.TerrainChunkBiomeHunkView; import art.arcane.iris.util.project.interpolation.InterpolationMethod; import art.arcane.iris.util.project.interpolation.InterpolationMethod3D; +import art.arcane.iris.util.project.interpolation.Interpolation3D; import art.arcane.iris.util.project.interpolation.IrisInterpolation; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.iris.util.common.parallel.MultiBurst; @@ -1162,7 +1163,7 @@ public interface Hunk extends HunkLike { Hunk::newArrayHunk, interpolated::toDouble, interpolated::fromDouble, - (x, y, z, s, noise) -> IrisInterpolation.getNoise3D(d, x, y, z, s, noise)); + (x, y, z, s, noise) -> Interpolation3D.getNoise3D(d, x, y, z, s, noise)); } /** diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/storage/PaletteOrHunk.java b/core/src/main/java/art/arcane/iris/util/project/hunk/storage/PaletteOrHunk.java deleted file mode 100644 index 5670a6638..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/hunk/storage/PaletteOrHunk.java +++ /dev/null @@ -1,37 +0,0 @@ -package art.arcane.iris.util.project.hunk.storage; - -import art.arcane.volmlib.util.function.Consumer4; -import art.arcane.volmlib.util.function.Consumer4IO; -import art.arcane.iris.util.project.hunk.Hunk; -import art.arcane.volmlib.util.hunk.bits.DataContainer; -import art.arcane.volmlib.util.hunk.bits.Writable; -import java.io.IOException; -import java.util.function.Supplier; - -public abstract class PaletteOrHunk extends art.arcane.volmlib.util.hunk.storage.PaletteOrHunk implements Hunk, Writable { - public PaletteOrHunk(int width, int height, int depth, boolean allow, Supplier> factory) { - super(width, height, depth, allow, factory::get); - } - - @Override - @SuppressWarnings("unchecked") - public DataContainer palette() { - return (DataContainer) super.palette(); - } - - public void setPalette(DataContainer c) { - super.setPalette(c); - } - - @Override - public PaletteOrHunk iterateSync(Consumer4 c) { - super.iterateSync(c); - return this; - } - - @Override - public PaletteOrHunk iterateSyncIO(Consumer4IO c) throws IOException { - super.iterateSyncIO(c); - return this; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java index c92a5ac57..010e192f7 100644 --- a/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/TerrainChunkBiomeHunkView.java @@ -18,10 +18,12 @@ package art.arcane.iris.util.project.hunk.view; +import art.arcane.iris.engine.data.chunk.LinkedTerrainChunk; import art.arcane.iris.engine.data.chunk.TerrainChunk; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.storage.StorageHunk; +import art.arcane.volmlib.util.hunk.HunkMutationSupport; public class TerrainChunkBiomeHunkView extends StorageHunk implements Hunk { private final TerrainChunk chunk; @@ -31,6 +33,21 @@ public class TerrainChunkBiomeHunkView extends StorageHunk implem this.chunk = chunk; } + /** + * A full height single column is the shape the biome actuator writes for every column of the chunk, so + * stride the backing biome array in one pass instead of dispatching per block. Any other region falls + * through to the generic element wise write. + */ + @Override + public void set(int x1, int y1, int z1, int x2, int y2, int z2, PlatformBiome biome) { + if (x1 == x2 && z1 == z2 && y1 == 0 && y2 == getHeight() - 1 && chunk instanceof LinkedTerrainChunk linked) { + linked.fillBiomeColumn(x1, z1, biome); + return; + } + + HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, biome); + } + @Override public void setRaw(int x, int y, int z, PlatformBiome biome) { chunk.setBiome(x, y + chunk.getMinHeight(), z, biome); diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java new file mode 100644 index 000000000..583be20bf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolation3D.java @@ -0,0 +1,322 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.function.NoiseProvider3; +import art.arcane.volmlib.util.interpolation.Starcast; + +/** + * Volumetric field samplers. The scalar kernels these compose (trilerp, tricubic, trihermite) live + * in {@link IrisInterpolation}; this class only owns the 3D sampling grids and the + * {@link InterpolationMethod3D} dispatch. + */ +public final class Interpolation3D { + private Interpolation3D() { + } + + public static double getStarcast3D(int x, int y, int z, double rad, double checks, NoiseProvider3 n) { + return (Starcast.starcast(x, z, rad, checks, (xx, zz) -> n.noise(xx, y, zz)) + + Starcast.starcast(x, y, rad, checks, (xx, yy) -> n.noise(xx, yy, z)) + + Starcast.starcast(y, z, rad, checks, (yy, zz) -> n.noise(x, yy, zz))) / 3D; + } + + public static double getTrilinear(int x, int y, int z, double rad, NoiseProvider3 n) { + return getTrilinear(x, y, z, rad, rad, rad, n); + } + + public static double getTrilinear(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + int fx = IrisInterpolation.getRadiusFactor(x, radx); + int fy = IrisInterpolation.getRadiusFactor(y, rady); + int fz = IrisInterpolation.getRadiusFactor(z, radz); + int x1 = (int) Math.round(fx * radx); + int y1 = (int) Math.round(fy * rady); + int z1 = (int) Math.round(fz * radz); + int x2 = (int) Math.round((fx + 1) * radx); + int y2 = (int) Math.round((fy + 1) * rady); + int z2 = (int) Math.round((fz + 1) * radz); + double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); + double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); + double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); + //@builder + return IrisInterpolation.trilerp( + n.noise(x1, y1, z1), + n.noise(x2, y1, z1), + n.noise(x1, y2, z1), + n.noise(x2, y2, z1), + n.noise(x1, y1, z2), + n.noise(x2, y1, z2), + n.noise(x1, y2, z2), + n.noise(x2, y2, z2), + px, py, pz); + //@done + } + + public static double getTricubic(int x, int y, int z, double rad, NoiseProvider3 n) { + return getTricubic(x, y, z, rad, rad, rad, n); + } + + public static double getTricubic(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + int fx = IrisInterpolation.getRadiusFactor(x, radx); + int fy = IrisInterpolation.getRadiusFactor(y, rady); + int fz = IrisInterpolation.getRadiusFactor(z, radz); + int x0 = (int) Math.round((fx - 1) * radx); + int y0 = (int) Math.round((fy - 1) * rady); + int z0 = (int) Math.round((fz - 1) * radz); + int x1 = (int) Math.round(fx * radx); + int y1 = (int) Math.round(fy * rady); + int z1 = (int) Math.round(fz * radz); + int x2 = (int) Math.round((fx + 1) * radx); + int y2 = (int) Math.round((fy + 1) * rady); + int z2 = (int) Math.round((fz + 1) * radz); + int x3 = (int) Math.round((fx + 2) * radx); + int y3 = (int) Math.round((fy + 2) * rady); + int z3 = (int) Math.round((fz + 2) * radz); + double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); + double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); + double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); + //@builder + //!!!!!!!!!!!!!!!!!! 2 1 3 + + return IrisInterpolation.tricubic( + n.noise(x0, y0, z0), + n.noise(x0, y1, z0), + n.noise(x0, y2, z0), + n.noise(x0, y3, z0), + n.noise(x1, y0, z0), + n.noise(x1, y1, z0), + n.noise(x1, y2, z0), + n.noise(x1, y3, z0), + n.noise(x2, y0, z0), + n.noise(x2, y1, z0), + n.noise(x2, y2, z0), + n.noise(x2, y3, z0), + n.noise(x3, y0, z0), + n.noise(x3, y1, z0), + n.noise(x3, y2, z0), + n.noise(x3, y3, z0), + n.noise(x0, y0, z1), + n.noise(x0, y1, z1), + n.noise(x0, y2, z1), + n.noise(x0, y3, z1), + n.noise(x1, y0, z1), + n.noise(x1, y1, z1), + n.noise(x1, y2, z1), + n.noise(x1, y3, z1), + n.noise(x2, y0, z1), + n.noise(x2, y1, z1), + n.noise(x2, y2, z1), + n.noise(x2, y3, z1), + n.noise(x3, y0, z1), + n.noise(x3, y1, z1), + n.noise(x3, y2, z1), + n.noise(x3, y3, z1), + n.noise(x0, y0, z2), + n.noise(x0, y1, z2), + n.noise(x0, y2, z2), + n.noise(x0, y3, z2), + n.noise(x1, y0, z2), + n.noise(x1, y1, z2), + n.noise(x1, y2, z2), + n.noise(x1, y3, z2), + n.noise(x2, y0, z2), + n.noise(x2, y1, z2), + n.noise(x2, y2, z2), + n.noise(x2, y3, z2), + n.noise(x3, y0, z2), + n.noise(x3, y1, z2), + n.noise(x3, y2, z2), + n.noise(x3, y3, z2), + n.noise(x0, y0, z3), + n.noise(x0, y1, z3), + n.noise(x0, y2, z3), + n.noise(x0, y3, z3), + n.noise(x1, y0, z3), + n.noise(x1, y1, z3), + n.noise(x1, y2, z3), + n.noise(x1, y3, z3), + n.noise(x2, y0, z3), + n.noise(x2, y1, z3), + n.noise(x2, y2, z3), + n.noise(x2, y3, z3), + n.noise(x3, y0, z3), + n.noise(x3, y1, z3), + n.noise(x3, y2, z3), + n.noise(x3, y3, z3), + px, py, pz); + //@done + } + + public static double getTrihermite(int x, int y, int z, double rad, NoiseProvider3 n, double tension, double bias) { + return getTrihermite(x, y, z, rad, rad, rad, n, tension, bias); + } + + public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + return getTrihermite(x, y, z, radx, rady, radz, n, 0D, 0D); + } + + public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n, double tension, double bias) { + int fx = IrisInterpolation.getRadiusFactor(x, radx); + int fy = IrisInterpolation.getRadiusFactor(y, rady); + int fz = IrisInterpolation.getRadiusFactor(z, radz); + int x0 = (int) Math.round((fx - 1) * radx); + int y0 = (int) Math.round((fy - 1) * rady); + int z0 = (int) Math.round((fz - 1) * radz); + int x1 = (int) Math.round(fx * radx); + int y1 = (int) Math.round(fy * rady); + int z1 = (int) Math.round(fz * radz); + int x2 = (int) Math.round((fx + 1) * radx); + int y2 = (int) Math.round((fy + 1) * rady); + int z2 = (int) Math.round((fz + 1) * radz); + int x3 = (int) Math.round((fx + 2) * radx); + int y3 = (int) Math.round((fy + 2) * rady); + int z3 = (int) Math.round((fz + 2) * radz); + double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); + double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); + double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); + //@builder + //!!!!!!!!!!!!!!!!!! 2 1 3 + + return IrisInterpolation.trihermite( + n.noise(x0, y0, z0), + n.noise(x0, y1, z0), + n.noise(x0, y2, z0), + n.noise(x0, y3, z0), + n.noise(x1, y0, z0), + n.noise(x1, y1, z0), + n.noise(x1, y2, z0), + n.noise(x1, y3, z0), + n.noise(x2, y0, z0), + n.noise(x2, y1, z0), + n.noise(x2, y2, z0), + n.noise(x2, y3, z0), + n.noise(x3, y0, z0), + n.noise(x3, y1, z0), + n.noise(x3, y2, z0), + n.noise(x3, y3, z0), + n.noise(x0, y0, z1), + n.noise(x0, y1, z1), + n.noise(x0, y2, z1), + n.noise(x0, y3, z1), + n.noise(x1, y0, z1), + n.noise(x1, y1, z1), + n.noise(x1, y2, z1), + n.noise(x1, y3, z1), + n.noise(x2, y0, z1), + n.noise(x2, y1, z1), + n.noise(x2, y2, z1), + n.noise(x2, y3, z1), + n.noise(x3, y0, z1), + n.noise(x3, y1, z1), + n.noise(x3, y2, z1), + n.noise(x3, y3, z1), + n.noise(x0, y0, z2), + n.noise(x0, y1, z2), + n.noise(x0, y2, z2), + n.noise(x0, y3, z2), + n.noise(x1, y0, z2), + n.noise(x1, y1, z2), + n.noise(x1, y2, z2), + n.noise(x1, y3, z2), + n.noise(x2, y0, z2), + n.noise(x2, y1, z2), + n.noise(x2, y2, z2), + n.noise(x2, y3, z2), + n.noise(x3, y0, z2), + n.noise(x3, y1, z2), + n.noise(x3, y2, z2), + n.noise(x3, y3, z2), + n.noise(x0, y0, z3), + n.noise(x0, y1, z3), + n.noise(x0, y2, z3), + n.noise(x0, y3, z3), + n.noise(x1, y0, z3), + n.noise(x1, y1, z3), + n.noise(x1, y2, z3), + n.noise(x1, y3, z3), + n.noise(x2, y0, z3), + n.noise(x2, y1, z3), + n.noise(x2, y2, z3), + n.noise(x2, y3, z3), + n.noise(x3, y0, z3), + n.noise(x3, y1, z3), + n.noise(x3, y2, z3), + n.noise(x3, y3, z3), + px, py, pz, tension, bias); + //@done + } + + public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { + return switch (method) { + case TRILINEAR -> getTrilinear(x, y, z, radx, rady, radz, n); + case TRICUBIC -> getTricubic(x, y, z, radx, rady, radz, n); + case TRIHERMITE -> getTrihermite(x, y, z, radx, rady, radz, n); + case TRISTARCAST_3 -> getStarcast3D(x, y, z, radx, 3D, n); + case TRISTARCAST_6 -> getStarcast3D(x, y, z, radx, 6D, n); + case TRISTARCAST_9 -> getStarcast3D(x, y, z, radx, 9D, n); + case TRISTARCAST_12 -> getStarcast3D(x, y, z, radx, 12D, n); + case TRILINEAR_TRISTARCAST_3 -> + getStarcast3D(x, y, z, radx, 3D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case TRILINEAR_TRISTARCAST_6 -> + getStarcast3D(x, y, z, radx, 6D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case TRILINEAR_TRISTARCAST_9 -> + getStarcast3D(x, y, z, radx, 9D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case TRILINEAR_TRISTARCAST_12 -> + getStarcast3D(x, y, z, radx, 12D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); + case NONE -> n.noise(x, y, z); + }; + } + + public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double rad, NoiseProvider3 n) { + return getNoise3D(method, xo, yo, zo, w, h, d, rad, rad, rad, n); + } + + /** + * Get the interpolated 3D noise within a given cuboid size with offsets + * + * @param method the interpolation method to use + * @param xo the x offset for noise + * @param yo the y offset for noise + * @param zo the z offset for noise + * @param w the width of the result + * @param h the height of the result + * @param d the depth of the result + * @param radX the interpolation radius for the x axis + * @param radY the interpolation radius for the y axis + * @param radZ the interpolation radius for the z axis + * @param n the noise provider + * @return the resulting hunk of noise + */ + public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double radX, double radY, double radZ, NoiseProvider3 n) { + Hunk hunk = Hunk.newAtomicDoubleHunk(w, h, d); + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + for (int k = 0; k < d; k++) { + hunk.set(i, j, k, getNoise3D(method, i + xo, j + yo, k + zo, radX, radY, radZ, n)); + } + } + } + + return hunk; + } + + public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double rad, NoiseProvider3 n) { + return getNoise3D(method, x, y, z, rad, rad, rad, n); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java index 6960a7a5c..a69e2071a 100644 --- a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java @@ -21,18 +21,9 @@ package art.arcane.iris.util.project.interpolation; import com.google.common.util.concurrent.AtomicDouble; import art.arcane.volmlib.util.interpolation.Starcast; import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.function.Consumer2; import art.arcane.volmlib.util.function.NoiseProvider; -import art.arcane.volmlib.util.function.NoiseProvider3; -import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; -import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; - -import java.math.BigDecimal; -import java.util.Arrays; public class IrisInterpolation { public static CNG cng = NoiseStyle.SIMPLEX.create(new RNG()); @@ -316,76 +307,6 @@ public class IrisInterpolation { //@done } - public static void test(String m, Consumer2 f) { - PrecisionStopwatch p = PrecisionStopwatch.start(); - - for (int i = 0; i < 8192; i++) { - f.accept(i, -i * 234); - } - - p.end(); - - IrisLogging.info("%s", m + ": " + Form.duration(p.getMilliseconds(), 8)); - } - - public static void printOptimizedSrc(boolean arrays) { - IrisLogging.info("%s", generateOptimizedStarcast(3, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(5, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(6, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(7, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(9, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(12, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(24, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(32, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(48, arrays)); - IrisLogging.info("%s", generateOptimizedStarcast(64, arrays)); - } - - public static String generateOptimizedStarcast(double checks, boolean array) { - double m = (360 / checks); - int ig = 0; - int igx = 0; - StringBuilder fb = new StringBuilder(); - StringBuilder sb = new StringBuilder(); - - if (array) { - fb.append("private static final double[] F").append((int) checks).append("A = {"); - } - - sb.append("private static double sc").append((int) checks).append("(int x, int z, double r, NoiseProvider n) {\n return ("); - for (int i = 0; i < 360; i += m) { - double sin = Math.sin(Math.toRadians(i)); - double cos = Math.cos(Math.toRadians(i)); - String cof = new BigDecimal(cos).toPlainString(); - String sif = new BigDecimal(sin).toPlainString(); - String cc = array ? "F" + (int) checks + "A[" + (igx++) + "]" : "F" + (int) checks + "C" + ig; - String ss = array ? "F" + (int) checks + "A[" + (igx++) + "]" : "F" + (int) checks + "S" + ig; - - if (array) { - fb.append(ig > 0 ? (ig % 6 == 0 ? ",\n" : ",") : "").append(cof).append(",").append(sif); - } else { - fb.append("private static final double ").append(cc).append(" = ").append(cof).append(";\n"); - fb.append("private static final double ").append(ss).append(" = ").append(sif).append(";\n"); - } - - sb.append(ig > 0 ? "\n +" : "").append("n.noise(x + ((r * ").append(cc).append(") - (r * ").append(ss).append(")), z + ((r * ").append(ss).append(") + (r * ").append(cc).append(")))"); - ig++; - } - - if (array) { - fb.append("};"); - } - - sb.append(")/").append(checks).append("D;\n}"); - return fb + "\n" + sb; - } - - public static double getStarcast3D(int x, int y, int z, double rad, double checks, NoiseProvider3 n) { - return (Starcast.starcast(x, z, rad, checks, (xx, zz) -> n.noise(xx, y, zz)) - + Starcast.starcast(x, y, rad, checks, (xx, yy) -> n.noise(xx, yy, z)) - + Starcast.starcast(y, z, rad, checks, (yy, zz) -> n.noise(x, yy, zz))) / 3D; - } - public static double getBilinearBezierNoise(int x, int z, double rad, NoiseProvider n) { int fx = getRadiusFactor(x, rad); int fz = getRadiusFactor(z, rad); @@ -424,235 +345,6 @@ public class IrisInterpolation { //@done } - public static double getTrilinear(int x, int y, int z, double rad, NoiseProvider3 n) { - return getTrilinear(x, y, z, rad, rad, rad, n); - } - - public static double getTrilinear(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - int fx = getRadiusFactor(x, radx); - int fy = getRadiusFactor(y, rady); - int fz = getRadiusFactor(z, radz); - int x1 = (int) Math.round(fx * radx); - int y1 = (int) Math.round(fy * rady); - int z1 = (int) Math.round(fz * radz); - int x2 = (int) Math.round((fx + 1) * radx); - int y2 = (int) Math.round((fy + 1) * rady); - int z2 = (int) Math.round((fz + 1) * radz); - double px = rangeScale(0, 1, x1, x2, x); - double py = rangeScale(0, 1, y1, y2, y); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - return trilerp( - n.noise(x1, y1, z1), - n.noise(x2, y1, z1), - n.noise(x1, y2, z1), - n.noise(x2, y2, z1), - n.noise(x1, y1, z2), - n.noise(x2, y1, z2), - n.noise(x1, y2, z2), - n.noise(x2, y2, z2), - px, py, pz); - //@done - } - - public static double getTricubic(int x, int y, int z, double rad, NoiseProvider3 n) { - return getTricubic(x, y, z, rad, rad, rad, n); - } - - public static double getTricubic(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - int fx = getRadiusFactor(x, radx); - int fy = getRadiusFactor(y, rady); - int fz = getRadiusFactor(z, radz); - int x0 = (int) Math.round((fx - 1) * radx); - int y0 = (int) Math.round((fy - 1) * rady); - int z0 = (int) Math.round((fz - 1) * radz); - int x1 = (int) Math.round(fx * radx); - int y1 = (int) Math.round(fy * rady); - int z1 = (int) Math.round(fz * radz); - int x2 = (int) Math.round((fx + 1) * radx); - int y2 = (int) Math.round((fy + 1) * rady); - int z2 = (int) Math.round((fz + 1) * radz); - int x3 = (int) Math.round((fx + 2) * radx); - int y3 = (int) Math.round((fy + 2) * rady); - int z3 = (int) Math.round((fz + 2) * radz); - double px = rangeScale(0, 1, x1, x2, x); - double py = rangeScale(0, 1, y1, y2, y); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - //!!!!!!!!!!!!!!!!!! 2 1 3 - - return tricubic( - n.noise(x0, y0, z0), - n.noise(x0, y1, z0), - n.noise(x0, y2, z0), - n.noise(x0, y3, z0), - n.noise(x1, y0, z0), - n.noise(x1, y1, z0), - n.noise(x1, y2, z0), - n.noise(x1, y3, z0), - n.noise(x2, y0, z0), - n.noise(x2, y1, z0), - n.noise(x2, y2, z0), - n.noise(x2, y3, z0), - n.noise(x3, y0, z0), - n.noise(x3, y1, z0), - n.noise(x3, y2, z0), - n.noise(x3, y3, z0), - n.noise(x0, y0, z1), - n.noise(x0, y1, z1), - n.noise(x0, y2, z1), - n.noise(x0, y3, z1), - n.noise(x1, y0, z1), - n.noise(x1, y1, z1), - n.noise(x1, y2, z1), - n.noise(x1, y3, z1), - n.noise(x2, y0, z1), - n.noise(x2, y1, z1), - n.noise(x2, y2, z1), - n.noise(x2, y3, z1), - n.noise(x3, y0, z1), - n.noise(x3, y1, z1), - n.noise(x3, y2, z1), - n.noise(x3, y3, z1), - n.noise(x0, y0, z2), - n.noise(x0, y1, z2), - n.noise(x0, y2, z2), - n.noise(x0, y3, z2), - n.noise(x1, y0, z2), - n.noise(x1, y1, z2), - n.noise(x1, y2, z2), - n.noise(x1, y3, z2), - n.noise(x2, y0, z2), - n.noise(x2, y1, z2), - n.noise(x2, y2, z2), - n.noise(x2, y3, z2), - n.noise(x3, y0, z2), - n.noise(x3, y1, z2), - n.noise(x3, y2, z2), - n.noise(x3, y3, z2), - n.noise(x0, y0, z3), - n.noise(x0, y1, z3), - n.noise(x0, y2, z3), - n.noise(x0, y3, z3), - n.noise(x1, y0, z3), - n.noise(x1, y1, z3), - n.noise(x1, y2, z3), - n.noise(x1, y3, z3), - n.noise(x2, y0, z3), - n.noise(x2, y1, z3), - n.noise(x2, y2, z3), - n.noise(x2, y3, z3), - n.noise(x3, y0, z3), - n.noise(x3, y1, z3), - n.noise(x3, y2, z3), - n.noise(x3, y3, z3), - px, py, pz); - //@done - } - - public static double getTrihermite(int x, int y, int z, double rad, NoiseProvider3 n, double tension, double bias) { - return getTrihermite(x, y, z, rad, rad, rad, n, tension, bias); - } - - public static double getTrihermite(int x, int y, int z, double rad, NoiseProvider3 n) { - return getTrihermite(x, y, z, rad, rad, rad, n, 0D, 0D); - } - - public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - return getTrihermite(x, y, z, radx, rady, radz, n, 0D, 0D); - } - - public static double getTrihermite(int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n, double tension, double bias) { - int fx = getRadiusFactor(x, radx); - int fy = getRadiusFactor(y, rady); - int fz = getRadiusFactor(z, radz); - int x0 = (int) Math.round((fx - 1) * radx); - int y0 = (int) Math.round((fy - 1) * rady); - int z0 = (int) Math.round((fz - 1) * radz); - int x1 = (int) Math.round(fx * radx); - int y1 = (int) Math.round(fy * rady); - int z1 = (int) Math.round(fz * radz); - int x2 = (int) Math.round((fx + 1) * radx); - int y2 = (int) Math.round((fy + 1) * rady); - int z2 = (int) Math.round((fz + 1) * radz); - int x3 = (int) Math.round((fx + 2) * radx); - int y3 = (int) Math.round((fy + 2) * rady); - int z3 = (int) Math.round((fz + 2) * radz); - double px = rangeScale(0, 1, x1, x2, x); - double py = rangeScale(0, 1, y1, y2, y); - double pz = rangeScale(0, 1, z1, z2, z); - //@builder - //!!!!!!!!!!!!!!!!!! 2 1 3 - - return trihermite( - n.noise(x0, y0, z0), - n.noise(x0, y1, z0), - n.noise(x0, y2, z0), - n.noise(x0, y3, z0), - n.noise(x1, y0, z0), - n.noise(x1, y1, z0), - n.noise(x1, y2, z0), - n.noise(x1, y3, z0), - n.noise(x2, y0, z0), - n.noise(x2, y1, z0), - n.noise(x2, y2, z0), - n.noise(x2, y3, z0), - n.noise(x3, y0, z0), - n.noise(x3, y1, z0), - n.noise(x3, y2, z0), - n.noise(x3, y3, z0), - n.noise(x0, y0, z1), - n.noise(x0, y1, z1), - n.noise(x0, y2, z1), - n.noise(x0, y3, z1), - n.noise(x1, y0, z1), - n.noise(x1, y1, z1), - n.noise(x1, y2, z1), - n.noise(x1, y3, z1), - n.noise(x2, y0, z1), - n.noise(x2, y1, z1), - n.noise(x2, y2, z1), - n.noise(x2, y3, z1), - n.noise(x3, y0, z1), - n.noise(x3, y1, z1), - n.noise(x3, y2, z1), - n.noise(x3, y3, z1), - n.noise(x0, y0, z2), - n.noise(x0, y1, z2), - n.noise(x0, y2, z2), - n.noise(x0, y3, z2), - n.noise(x1, y0, z2), - n.noise(x1, y1, z2), - n.noise(x1, y2, z2), - n.noise(x1, y3, z2), - n.noise(x2, y0, z2), - n.noise(x2, y1, z2), - n.noise(x2, y2, z2), - n.noise(x2, y3, z2), - n.noise(x3, y0, z2), - n.noise(x3, y1, z2), - n.noise(x3, y2, z2), - n.noise(x3, y3, z2), - n.noise(x0, y0, z3), - n.noise(x0, y1, z3), - n.noise(x0, y2, z3), - n.noise(x0, y3, z3), - n.noise(x1, y0, z3), - n.noise(x1, y1, z3), - n.noise(x1, y2, z3), - n.noise(x1, y3, z3), - n.noise(x2, y0, z3), - n.noise(x2, y1, z3), - n.noise(x2, y2, z3), - n.noise(x2, y3, z3), - n.noise(x3, y0, z3), - n.noise(x3, y1, z3), - n.noise(x3, y2, z3), - n.noise(x3, y3, z3), - px, py, pz, tension, bias); - //@done - } - public static double getBilinearCenterSineNoise(int x, int z, double rad, NoiseProvider n) { int fx = getRadiusFactor(x, rad); int fz = getRadiusFactor(z, rad); @@ -909,64 +601,6 @@ public class IrisInterpolation { return rad.get(); } - public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double radx, double rady, double radz, NoiseProvider3 n) { - return switch (method) { - case TRILINEAR -> getTrilinear(x, y, z, radx, rady, radz, n); - case TRICUBIC -> getTricubic(x, y, z, radx, rady, radz, n); - case TRIHERMITE -> getTrihermite(x, y, z, radx, rady, radz, n); - case TRISTARCAST_3 -> getStarcast3D(x, y, z, radx, 3D, n); - case TRISTARCAST_6 -> getStarcast3D(x, y, z, radx, 6D, n); - case TRISTARCAST_9 -> getStarcast3D(x, y, z, radx, 9D, n); - case TRISTARCAST_12 -> getStarcast3D(x, y, z, radx, 12D, n); - case TRILINEAR_TRISTARCAST_3 -> - getStarcast3D(x, y, z, radx, 3D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case TRILINEAR_TRISTARCAST_6 -> - getStarcast3D(x, y, z, radx, 6D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case TRILINEAR_TRISTARCAST_9 -> - getStarcast3D(x, y, z, radx, 9D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case TRILINEAR_TRISTARCAST_12 -> - getStarcast3D(x, y, z, radx, 12D, (xx, yy, zz) -> getTrilinear((int) xx, (int) yy, (int) zz, radx, rady, radz, n)); - case NONE -> n.noise(x, y, z); - }; - } - - public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double rad, NoiseProvider3 n) { - return getNoise3D(method, xo, yo, zo, w, h, d, rad, rad, rad, n); - } - - /** - * Get the interpolated 3D noise within a given cuboid size with offsets - * - * @param method the interpolation method to use - * @param xo the x offset for noise - * @param yo the y offset for noise - * @param zo the z offset for noise - * @param w the width of the result - * @param h the height of the result - * @param d the depth of the result - * @param radX the interpolation radius for the x axis - * @param radY the interpolation radius for the y axis - * @param radZ the interpolation radius for the z axis - * @param n the noise provider - * @return the resulting hunk of noise - */ - public static Hunk getNoise3D(InterpolationMethod3D method, int xo, int yo, int zo, int w, int h, int d, double radX, double radY, double radZ, NoiseProvider3 n) { - Hunk hunk = Hunk.newAtomicDoubleHunk(w, h, d); - for (int i = 0; i < w; i++) { - for (int j = 0; j < h; j++) { - for (int k = 0; k < d; k++) { - hunk.set(i, j, k, getNoise3D(method, i + xo, j + yo, k + zo, radX, radY, radZ, n)); - } - } - } - - return hunk; - } - - public static double getNoise3D(InterpolationMethod3D method, int x, int y, int z, double rad, NoiseProvider3 n) { - return getNoise3D(method, x, y, z, rad, rad, rad, n); - } - public static double getNoise(InterpolationMethod method, int x, int z, double h, NoiseProvider noise) { final NoiseProvider n; if (usesSampleCache(method)) { @@ -977,71 +611,56 @@ public class IrisInterpolation { n = noise; } - if (method.equals(InterpolationMethod.BILINEAR)) { - return getBilinearNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.STARCAST_3)) { - return Starcast.starcast(x, z, h, 3D, n); - } else if (method.equals(InterpolationMethod.STARCAST_6)) { - return Starcast.starcast(x, z, h, 6D, n); - } else if (method.equals(InterpolationMethod.STARCAST_9)) { - return Starcast.starcast(x, z, h, 9D, n); - } else if (method.equals(InterpolationMethod.STARCAST_12)) { - return Starcast.starcast(x, z, h, 12D, n); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_3)) { - return Starcast.starcast(x, z, h, 3D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_6)) { - return Starcast.starcast(x, z, h, 6D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_9)) { - return Starcast.starcast(x, z, h, 9D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.BILINEAR_STARCAST_12)) { - return Starcast.starcast(x, z, h, 12D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_3)) { - return Starcast.starcast(x, z, h, 3D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_6)) { - return Starcast.starcast(x, z, h, 6D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_9)) { - return Starcast.starcast(x, z, h, 9D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.HERMITE_STARCAST_12)) { - return Starcast.starcast(x, z, h, 12D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); - } else if (method.equals(InterpolationMethod.BILINEAR_BEZIER)) { - return getBilinearBezierNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.BILINEAR_PARAMETRIC_2)) { - return getBilinearParametricNoise(x, z, h, n, 2); - } else if (method.equals(InterpolationMethod.BILINEAR_PARAMETRIC_4)) { - return getBilinearParametricNoise(x, z, h, n, 4); - } else if (method.equals(InterpolationMethod.BILINEAR_PARAMETRIC_1_5)) { - return getBilinearParametricNoise(x, z, h, n, 1.5); - } else if (method.equals(InterpolationMethod.BICUBIC)) { - return getBilinearNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.HERMITE)) { - return getHermiteNoise(x, z, h, n); - } else if (method.equals(InterpolationMethod.HERMITE_TENSE)) { - return getHermiteNoise(x, z, h, n, 0.8D, 0D); - } else if (method.equals(InterpolationMethod.CATMULL_ROM_SPLINE)) { - return getHermiteNoise(x, z, h, n, 1D, 0D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE)) { - return getHermiteNoise(x, z, h, n, 0D, 0D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_HALF_NEGATIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, -0.5D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_HALF_POSITIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, 0.5D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_FULL_NEGATIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, -1D); - } else if (method.equals(InterpolationMethod.HERMITE_LOOSE_FULL_POSITIVE_BIAS)) { - return getHermiteNoise(x, z, h, n, 0D, 1D); - } - - return n.noise(x, z); + return switch (method) { + case BILINEAR -> getBilinearNoise(x, z, h, n); + case STARCAST_3 -> Starcast.starcast(x, z, h, 3D, n); + case STARCAST_6 -> Starcast.starcast(x, z, h, 6D, n); + case STARCAST_9 -> Starcast.starcast(x, z, h, 9D, n); + case STARCAST_12 -> Starcast.starcast(x, z, h, 12D, n); + case BILINEAR_STARCAST_3 -> + Starcast.starcast(x, z, h, 3D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case BILINEAR_STARCAST_6 -> + Starcast.starcast(x, z, h, 6D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case BILINEAR_STARCAST_9 -> + Starcast.starcast(x, z, h, 9D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case BILINEAR_STARCAST_12 -> + Starcast.starcast(x, z, h, 12D, (xx, zz) -> getBilinearNoise((int) xx, (int) zz, h, n)); + case HERMITE_STARCAST_3 -> + Starcast.starcast(x, z, h, 3D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case HERMITE_STARCAST_6 -> + Starcast.starcast(x, z, h, 6D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case HERMITE_STARCAST_9 -> + Starcast.starcast(x, z, h, 9D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case HERMITE_STARCAST_12 -> + Starcast.starcast(x, z, h, 12D, (xx, zz) -> getHermiteNoise((int) xx, (int) zz, h, n, 0D, 0D)); + case BILINEAR_BEZIER -> getBilinearBezierNoise(x, z, h, n); + case BILINEAR_PARAMETRIC_2 -> getBilinearParametricNoise(x, z, h, n, 2); + case BILINEAR_PARAMETRIC_4 -> getBilinearParametricNoise(x, z, h, n, 4); + case BILINEAR_PARAMETRIC_1_5 -> getBilinearParametricNoise(x, z, h, n, 1.5); + case BICUBIC -> getBilinearNoise(x, z, h, n); + case HERMITE -> getHermiteNoise(x, z, h, n); + case HERMITE_TENSE -> getHermiteNoise(x, z, h, n, 0.8D, 0D); + case CATMULL_ROM_SPLINE -> getHermiteNoise(x, z, h, n, 1D, 0D); + case HERMITE_LOOSE -> getHermiteNoise(x, z, h, n, 0D, 0D); + case HERMITE_LOOSE_HALF_NEGATIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, -0.5D); + case HERMITE_LOOSE_HALF_POSITIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, 0.5D); + case HERMITE_LOOSE_FULL_NEGATIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, -1D); + case HERMITE_LOOSE_FULL_POSITIVE_BIAS -> getHermiteNoise(x, z, h, n, 0D, 1D); + case NONE -> n.noise(x, z); + }; } public static NoiseBounds getNoiseBounds(InterpolationMethod method, int x, int z, double h, NoiseBoundsProvider noise) { NoiseBoundsSampleCache2D cache = NOISE_BOUNDS_SAMPLE_CACHE_2D.get(); cache.clear(); - NoiseProvider minProvider = (sampleX, sampleZ) -> cache.getOrSampleMin(sampleX, sampleZ, noise); - NoiseProvider maxProvider = (sampleX, sampleZ) -> cache.getOrSampleMax(sampleX, sampleZ, noise); - double min = getNoise(method, x, z, h, minProvider); - double max = getNoise(method, x, z, h, maxProvider); - return new NoiseBounds(min, max); + NoiseBoundsProvider previous = cache.bindProvider(noise); + try { + double min = getNoise(method, x, z, h, cache.minView()); + double max = getNoise(method, x, z, h, cache.maxView()); + return new NoiseBounds(min, max); + } finally { + cache.bindProvider(previous); + } } private static boolean usesSampleCache(InterpolationMethod method) { @@ -1058,295 +677,7 @@ public class IrisInterpolation { }; } - private static class NoiseSampleCache2D { - private long[] xBits; - private long[] zBits; - private double[] values; - private byte[] states; - private int mask; - private int resizeThreshold; - private int size; - - public NoiseSampleCache2D(int initialCapacity) { - int minimumCapacity = Math.max(8, initialCapacity); - int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); - xBits = new long[tableSize]; - zBits = new long[tableSize]; - values = new double[tableSize]; - states = new byte[tableSize]; - mask = tableSize - 1; - resizeThreshold = Math.max(1, (tableSize * 3) >> 2); - size = 0; - } - - public void clear() { - if (size == 0) { - return; - } - Arrays.fill(states, (byte) 0); - size = 0; - } - - public double getOrSample(double relativeX, double relativeZ, double sampleX, double sampleZ, NoiseProvider provider) { - long rx = Double.doubleToLongBits(relativeX); - long rz = Double.doubleToLongBits(relativeZ); - int slot = findSlot(rx, rz); - if (states[slot] != 0) { - return values[slot]; - } - - double value = provider.noise(sampleX, sampleZ); - insert(slot, rx, rz, value); - return value; - } - - private int findSlot(long rx, long rz) { - int slot = mix(rx, rz) & mask; - while (states[slot] != 0) { - if (xBits[slot] == rx && zBits[slot] == rz) { - break; - } - slot = (slot + 1) & mask; - } - return slot; - } - - private void insert(int slot, long rx, long rz, double value) { - xBits[slot] = rx; - zBits[slot] = rz; - values[slot] = value; - states[slot] = 1; - size++; - if (size >= resizeThreshold) { - grow(); - } - } - - private int mix(long rx, long rz) { - long hash = rx * 0x9E3779B97F4A7C15L; - hash ^= Long.rotateLeft(rz * 0xC2B2AE3D27D4EB4FL, 32); - hash ^= (hash >>> 33); - hash *= 0xff51afd7ed558ccdL; - hash ^= (hash >>> 33); - return (int) hash; - } - - private void grow() { - long[] previousXBits = xBits; - long[] previousZBits = zBits; - double[] previousValues = values; - byte[] previousStates = states; - - int nextLength = xBits.length << 1; - long[] nextXBits = new long[nextLength]; - long[] nextZBits = new long[nextLength]; - double[] nextValues = new double[nextLength]; - byte[] nextStates = new byte[nextLength]; - - xBits = nextXBits; - zBits = nextZBits; - values = nextValues; - states = nextStates; - mask = nextLength - 1; - resizeThreshold = Math.max(1, (nextLength * 3) >> 2); - size = 0; - - for (int i = 0; i < previousStates.length; i++) { - if (previousStates[i] == 0) { - continue; - } - int slot = findSlot(previousXBits[i], previousZBits[i]); - xBits[slot] = previousXBits[i]; - zBits[slot] = previousZBits[i]; - values[slot] = previousValues[i]; - states[slot] = 1; - size++; - } - } - - private int tableSizeFor(int value) { - int n = value - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - int size = n + 1; - if (size < 8) { - return 8; - } - return size; - } - } - - @FunctionalInterface - public interface NoiseBoundsProvider { - NoiseBounds noise(double x, double z); - } - - public static final class NoiseBounds { - private final double min; - private final double max; - - public NoiseBounds(double min, double max) { - this.min = min; - this.max = max; - } - - public double min() { - return min; - } - - public double max() { - return max; - } - } - - private static class NoiseBoundsSampleCache2D { - private long[] xBits; - private long[] zBits; - private double[] minValues; - private double[] maxValues; - private byte[] states; - private int mask; - private int resizeThreshold; - private int size; - - public NoiseBoundsSampleCache2D(int initialCapacity) { - int minimumCapacity = Math.max(8, initialCapacity); - int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); - xBits = new long[tableSize]; - zBits = new long[tableSize]; - minValues = new double[tableSize]; - maxValues = new double[tableSize]; - states = new byte[tableSize]; - mask = tableSize - 1; - resizeThreshold = Math.max(1, (tableSize * 3) >> 2); - size = 0; - } - - public void clear() { - if (size == 0) { - return; - } - Arrays.fill(states, (byte) 0); - size = 0; - } - - public double getOrSampleMin(double sampleX, double sampleZ, NoiseBoundsProvider provider) { - long xBitsValue = Double.doubleToLongBits(sampleX); - long zBitsValue = Double.doubleToLongBits(sampleZ); - int slot = findSlot(xBitsValue, zBitsValue); - if (states[slot] != 0) { - return minValues[slot]; - } - - NoiseBounds bounds = provider.noise(sampleX, sampleZ); - insert(slot, xBitsValue, zBitsValue, bounds.min(), bounds.max()); - return bounds.min(); - } - - public double getOrSampleMax(double sampleX, double sampleZ, NoiseBoundsProvider provider) { - long xBitsValue = Double.doubleToLongBits(sampleX); - long zBitsValue = Double.doubleToLongBits(sampleZ); - int slot = findSlot(xBitsValue, zBitsValue); - if (states[slot] != 0) { - return maxValues[slot]; - } - - NoiseBounds bounds = provider.noise(sampleX, sampleZ); - insert(slot, xBitsValue, zBitsValue, bounds.min(), bounds.max()); - return bounds.max(); - } - - private int findSlot(long xb, long zb) { - int slot = mix(xb, zb) & mask; - while (states[slot] != 0) { - if (xBits[slot] == xb && zBits[slot] == zb) { - break; - } - slot = (slot + 1) & mask; - } - return slot; - } - - private void insert(int slot, long xb, long zb, double min, double max) { - xBits[slot] = xb; - zBits[slot] = zb; - minValues[slot] = min; - maxValues[slot] = max; - states[slot] = 1; - size++; - if (size >= resizeThreshold) { - grow(); - } - } - - private int mix(long xb, long zb) { - long hash = xb * 0x9E3779B97F4A7C15L; - hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32); - hash ^= (hash >>> 33); - hash *= 0xff51afd7ed558ccdL; - hash ^= (hash >>> 33); - return (int) hash; - } - - private void grow() { - long[] previousXBits = xBits; - long[] previousZBits = zBits; - double[] previousMin = minValues; - double[] previousMax = maxValues; - byte[] previousStates = states; - - int nextLength = xBits.length << 1; - long[] nextXBits = new long[nextLength]; - long[] nextZBits = new long[nextLength]; - double[] nextMin = new double[nextLength]; - double[] nextMax = new double[nextLength]; - byte[] nextStates = new byte[nextLength]; - - xBits = nextXBits; - zBits = nextZBits; - minValues = nextMin; - maxValues = nextMax; - states = nextStates; - mask = nextLength - 1; - resizeThreshold = Math.max(1, (nextLength * 3) >> 2); - size = 0; - - for (int i = 0; i < previousStates.length; i++) { - if (previousStates[i] == 0) { - continue; - } - int slot = findSlot(previousXBits[i], previousZBits[i]); - xBits[slot] = previousXBits[i]; - zBits[slot] = previousZBits[i]; - minValues[slot] = previousMin[i]; - maxValues[slot] = previousMax[i]; - states[slot] = 1; - size++; - } - } - - private int tableSizeFor(int value) { - int n = value - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - int tableSize = n + 1; - if (tableSize < 8) { - return 8; - } - return tableSize; - } - } - public static double rangeScale(double amin, double amax, double bmin, double bmax, double b) { return amin + ((amax - amin) * ((b - bmin) / (bmax - bmin))); } - - public record NoiseKey(double x, double z) { - } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleSized.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java similarity index 80% rename from core/src/main/java/art/arcane/iris/engine/mantle/MantleSized.java rename to core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java index 9a4d6d09d..c1a1e0c5f 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleSized.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java @@ -16,8 +16,10 @@ * along with this program. If not, see . */ -package art.arcane.iris.engine.mantle; +package art.arcane.iris.util.project.interpolation; -public interface MantleSized { - int getMaxChunkSize(); +/** + * An immutable minimum/maximum noise pair produced by bounds interpolation. + */ +public record NoiseBounds(double min, double max) { } diff --git a/core/src/main/java/art/arcane/iris/util/common/data/palette/CountConsumer.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java similarity index 85% rename from core/src/main/java/art/arcane/iris/util/common/data/palette/CountConsumer.java rename to core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java index 9038fc186..ee0952ca1 100644 --- a/core/src/main/java/art/arcane/iris/util/common/data/palette/CountConsumer.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package art.arcane.iris.util.common.data.palette; +package art.arcane.iris.util.project.interpolation; @FunctionalInterface -public interface CountConsumer { - void accept(T paramT, int paramInt); +public interface NoiseBoundsProvider { + NoiseBounds noise(double x, double z); } diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java new file mode 100644 index 000000000..49e30f610 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java @@ -0,0 +1,201 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import art.arcane.volmlib.util.function.NoiseProvider; + +import java.util.Arrays; + +/** + * Open-addressed memo table that samples a {@link NoiseBoundsProvider} once per column and serves + * both the min and max side from the same entry. Single threaded by contract; instances are held in + * thread locals. + *

+ * The min/max {@link NoiseProvider} views are allocated once per cache instance and read the + * currently bound provider, so a bounds interpolation pass allocates no lambdas per column. + */ +final class NoiseBoundsSampleCache2D { + private final NoiseProvider minView = this::sampleMin; + private final NoiseProvider maxView = this::sampleMax; + private NoiseBoundsProvider boundProvider; + private long[] xBits; + private long[] zBits; + private double[] minValues; + private double[] maxValues; + private byte[] states; + private int mask; + private int resizeThreshold; + private int size; + + public NoiseBoundsSampleCache2D(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + minValues = new double[tableSize]; + maxValues = new double[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + public void clear() { + if (size == 0) { + return; + } + Arrays.fill(states, (byte) 0); + size = 0; + } + + /** + * Binds the provider that {@link #minView()} and {@link #maxView()} delegate to, returning the + * previously bound provider so callers can restore it. + */ + public NoiseBoundsProvider bindProvider(NoiseBoundsProvider provider) { + NoiseBoundsProvider previous = boundProvider; + boundProvider = provider; + return previous; + } + + public NoiseProvider minView() { + return minView; + } + + public NoiseProvider maxView() { + return maxView; + } + + private double sampleMin(double sampleX, double sampleZ) { + return getOrSampleMin(sampleX, sampleZ, boundProvider); + } + + private double sampleMax(double sampleX, double sampleZ) { + return getOrSampleMax(sampleX, sampleZ, boundProvider); + } + + public double getOrSampleMin(double sampleX, double sampleZ, NoiseBoundsProvider provider) { + long xBitsValue = Double.doubleToLongBits(sampleX); + long zBitsValue = Double.doubleToLongBits(sampleZ); + int slot = findSlot(xBitsValue, zBitsValue); + if (states[slot] != 0) { + return minValues[slot]; + } + + NoiseBounds bounds = provider.noise(sampleX, sampleZ); + insert(slot, xBitsValue, zBitsValue, bounds.min(), bounds.max()); + return bounds.min(); + } + + public double getOrSampleMax(double sampleX, double sampleZ, NoiseBoundsProvider provider) { + long xBitsValue = Double.doubleToLongBits(sampleX); + long zBitsValue = Double.doubleToLongBits(sampleZ); + int slot = findSlot(xBitsValue, zBitsValue); + if (states[slot] != 0) { + return maxValues[slot]; + } + + NoiseBounds bounds = provider.noise(sampleX, sampleZ); + insert(slot, xBitsValue, zBitsValue, bounds.min(), bounds.max()); + return bounds.max(); + } + + private int findSlot(long xb, long zb) { + int slot = mix(xb, zb) & mask; + while (states[slot] != 0) { + if (xBits[slot] == xb && zBits[slot] == zb) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private void insert(int slot, long xb, long zb, double min, double max) { + xBits[slot] = xb; + zBits[slot] = zb; + minValues[slot] = min; + maxValues[slot] = max; + states[slot] = 1; + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int mix(long xb, long zb) { + long hash = xb * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + double[] previousMin = minValues; + double[] previousMax = maxValues; + byte[] previousStates = states; + + int nextLength = xBits.length << 1; + long[] nextXBits = new long[nextLength]; + long[] nextZBits = new long[nextLength]; + double[] nextMin = new double[nextLength]; + double[] nextMax = new double[nextLength]; + byte[] nextStates = new byte[nextLength]; + + xBits = nextXBits; + zBits = nextZBits; + minValues = nextMin; + maxValues = nextMax; + states = nextStates; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + minValues[slot] = previousMin[i]; + maxValues[slot] = previousMax[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int tableSize = n + 1; + if (tableSize < 8) { + return 8; + } + return tableSize; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java new file mode 100644 index 000000000..e8ecf5203 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java @@ -0,0 +1,149 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.util.project.interpolation; + +import art.arcane.volmlib.util.function.NoiseProvider; + +import java.util.Arrays; + +/** + * Open-addressed memo table keyed on a relative sample offset, used to collapse the duplicate + * noise probes that starcast composites issue for the same column. Single threaded by contract; + * instances are held in thread locals. + */ +final class NoiseSampleCache2D { + private long[] xBits; + private long[] zBits; + private double[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int size; + + public NoiseSampleCache2D(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + values = new double[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + public void clear() { + if (size == 0) { + return; + } + Arrays.fill(states, (byte) 0); + size = 0; + } + + public double getOrSample(double relativeX, double relativeZ, double sampleX, double sampleZ, NoiseProvider provider) { + long rx = Double.doubleToLongBits(relativeX); + long rz = Double.doubleToLongBits(relativeZ); + int slot = findSlot(rx, rz); + if (states[slot] != 0) { + return values[slot]; + } + + double value = provider.noise(sampleX, sampleZ); + insert(slot, rx, rz, value); + return value; + } + + private int findSlot(long rx, long rz) { + int slot = mix(rx, rz) & mask; + while (states[slot] != 0) { + if (xBits[slot] == rx && zBits[slot] == rz) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private void insert(int slot, long rx, long rz, double value) { + xBits[slot] = rx; + zBits[slot] = rz; + values[slot] = value; + states[slot] = 1; + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int mix(long rx, long rz) { + long hash = rx * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(rz * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + double[] previousValues = values; + byte[] previousStates = states; + + int nextLength = xBits.length << 1; + long[] nextXBits = new long[nextLength]; + long[] nextZBits = new long[nextLength]; + double[] nextValues = new double[nextLength]; + byte[] nextStates = new byte[nextLength]; + + xBits = nextXBits; + zBits = nextZBits; + values = nextValues; + states = nextStates; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + values[slot] = previousValues[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int size = n + 1; + if (size < 8) { + return 8; + } + return size; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java index 92a7e415c..9574f36a4 100644 --- a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java @@ -51,8 +51,6 @@ public class CNG { public static final NoiseInjector SRC_POW = (s, v) -> new double[]{Math.pow(s, v), 0}; public static final NoiseInjector DST_MOD = (s, v) -> new double[]{v % s, 0}; public static final NoiseInjector DST_POW = (s, v) -> new double[]{Math.pow(v, s), 0}; - public static long hits = 0; - public static long creates = 0; private final double opacity; private double scale; private double bakedScale; @@ -99,7 +97,6 @@ public class CNG { public CNG(RNG random, NoiseGenerator generator, double opacity, int octaves) { customGenerator = null; - creates++; noscale = generator.isNoScale(); this.oct = octaves; this.rng = random; @@ -739,7 +736,6 @@ public class CNG { private double applyPost(double n, double x) { n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; double m = 1; - hits += oct; if (children != null) { for (CNG i : children) { @@ -780,7 +776,6 @@ public class CNG { private double applyPost(double n, double x, double z) { n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; double m = 1; - hits += oct; if (children != null) { for (CNG i : children) { @@ -821,7 +816,6 @@ public class CNG { private double applyPost(double n, double x, double y, double z) { n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; double m = 1; - hits += oct; if (children != null) { for (CNG i : children) { @@ -875,7 +869,6 @@ public class CNG { double n = getNoise(dim); n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; double m = 1; - hits += oct; if (children == null) { return (n - down + up) * patch; } diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CachedNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CachedNoise.java deleted file mode 100644 index 1a03346c8..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/noise/CachedNoise.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.project.noise; - -public class CachedNoise implements NoiseGenerator { - private final CachedNoiseMap n; - - public CachedNoise(NoiseGenerator generator, int size) { - n = new CachedNoiseMap(size, generator); - } - - @Override - public double noise(double x) { - return n.get((int) Math.round(x), 0); - } - - @Override - public double noise(double x, double z) { - return n.get((int) Math.round(x), (int) Math.round(z)); - } - - @Override - public double noise(double x, double y, double z) { - return n.get((int) Math.round(x), (int) Math.round(z)); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CachedNoiseMap.java b/core/src/main/java/art/arcane/iris/util/project/noise/CachedNoiseMap.java deleted file mode 100644 index 882f0ee09..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/noise/CachedNoiseMap.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.project.noise; - -import art.arcane.volmlib.util.hunk.bits.Writable; -import art.arcane.volmlib.util.matter.IrisMatter; -import art.arcane.volmlib.util.matter.Matter; -import art.arcane.volmlib.util.matter.MatterSlice; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; - -public class CachedNoiseMap implements Writable { - private final Matter noise; - private final MatterSlice slice; - - public CachedNoiseMap(int size, NoiseGenerator cng) { - noise = new IrisMatter(size, size, 1); - slice = noise.slice(Integer.class); - - for (int i = 0; i < slice.getWidth(); i++) { - for (int j = 0; j < slice.getHeight(); j++) { - set(i, j, cng.noise(i, j)); - } - } - } - - public CachedNoiseMap(File file) throws IOException, ClassNotFoundException { - noise = Matter.read(file); - slice = noise.slice(Integer.class); - } - - void write(File file) throws IOException { - noise.write(file); - } - - void set(int x, int y, double value) { - slice.set(x % slice.getWidth(), y % slice.getHeight(), 0, Float.floatToIntBits((float) value)); - } - - double get(int x, int y) { - Integer i = slice.get(x % slice.getWidth(), y % slice.getHeight(), 0); - - if (i == null) { - return 0; - } - - return Float.intBitsToFloat(i); - } - - @Override - public Integer readNodeData(DataInputStream din) throws IOException { - return din.readInt(); - } - - @Override - public void writeNodeData(DataOutputStream dos, Integer integer) throws IOException { - dos.writeInt(integer); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/RarityCellGenerator.java b/core/src/main/java/art/arcane/iris/util/project/noise/RarityCellGenerator.java deleted file mode 100644 index 2557e865b..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/noise/RarityCellGenerator.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.project.noise; - -import art.arcane.iris.engine.object.IRare; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.math.RNG; - -public class RarityCellGenerator extends CellGenerator { - public RarityCellGenerator(RNG rng) { - super(rng); - } - - public T get(double x, double z, KList b) { - if (b.size() == 0) { - return null; - } - - if (b.size() == 1) { - return b.get(0); - } - - KList rarityMapped = new KList<>(); - boolean o = false; - int max = 1; - for (T i : b) { - if (i.getRarity() > max) { - max = i.getRarity(); - } - } - - max++; - - for (T i : b) { - for (int j = 0; j < max - i.getRarity(); j++) { - //noinspection AssignmentUsedAsCondition - if (o = !o) { - rarityMapped.add(i); - } else { - rarityMapped.add(0, i); - } - } - } - - if (rarityMapped.size() == 1) { - return rarityMapped.get(0); - } - - if (rarityMapped.isEmpty()) { - throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); - } - - return rarityMapped.get(getIndex(x, z, rarityMapped.size())); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java b/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java deleted file mode 100644 index 86322fed5..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/profile/MsptTimings.java +++ /dev/null @@ -1,84 +0,0 @@ -package art.arcane.iris.util.project.profile; - -import art.arcane.volmlib.util.math.M; -import art.arcane.iris.util.common.scheduling.J; -import art.arcane.volmlib.util.scheduling.Looper; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -public abstract class MsptTimings extends Looper { - private final AtomicInteger currentTick = new AtomicInteger(0); - private int lastTick, lastMspt; - private long lastTime; - private int taskId = -1; - - public MsptTimings() { - setName("MsptTimings"); - setPriority(9); - setDaemon(true); - } - - public static MsptTimings of(Consumer update) { - return new Simple(update); - } - - @Override - protected final long loop() { - if (startTickTask()) - return 200; - - long now = M.ms(); - int tick = currentTick.get(); - int deltaTick = tick - lastTick; - if (deltaTick == 0) - return 200; - lastTick = tick; - int deltaTime = (int) (now - lastTime); - lastTime = now; - int mspt = deltaTime / deltaTick; - mspt -= 50; - mspt = Math.max(mspt, 0); - lastMspt = mspt; - update(mspt); - return 200; - } - - public final int getMspt() { - return lastMspt; - } - - protected abstract void update(int mspt); - - private boolean startTickTask() { - if (taskId != -1) - return false; - - taskId = J.sr(() -> { - if (isInterrupted()) { - J.csr(taskId); - taskId = -1; - return; - } - - currentTick.incrementAndGet(); - }, 1); - return taskId != -1; - } - - private static class Simple extends MsptTimings { - private final Consumer update; - - private Simple(Consumer update) { - this.update = update; - start(); - } - - @Override - protected void update(int mspt) { - if (update == null) - return; - update.accept(mspt); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java index 99ec07a44..5f5c24d1b 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java @@ -561,7 +561,7 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { } default void fill(Hunk h, double x, double y, double z) { - fill(h, x, z, 4); + fill(h, x, y, z, 4); } default void fill2D(Hunk h, double x, double z, V v) { @@ -610,7 +610,7 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { for (int i = 0; i < 16; i++) { for (int j = 0; j < 16; j++) { - c[Cache.to1D(i + xs, j + zs, 0, 16, 16)] = get(i + xs, j + zs); + c[Cache.to1D(i, j, 0, 16, 16)] = get(i + xs, j + zs); } } } diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriHermiteStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriHermiteStream.java deleted file mode 100644 index ded45d4cd..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/stream/interpolation/TriHermiteStream.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.project.stream.interpolation; - -import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import art.arcane.iris.util.project.stream.BasicStream; -import art.arcane.iris.util.project.stream.ProceduralStream; -public class TriHermiteStream extends BasicStream implements Interpolator { - private final int rx; - private final int ry; - private final int rz; - private final double tension; - private final double bias; - - public TriHermiteStream(ProceduralStream stream, int rx, int ry, int rz, double tension, double bias) { - super(stream); - this.rx = rx; - this.ry = ry; - this.rz = rz; - this.tension = tension; - this.bias = bias; - } - - public T interpolate(double x, double y, double z) { - int fx = (int) Math.floor(x / rx); - int fy = (int) Math.floor(y / ry); - int fz = (int) Math.floor(z / rz); - int x0 = Math.round((fx - 1) * rx); - int y0 = Math.round((fy - 1) * ry); - int z0 = Math.round((fz - 1) * rz); - int x1 = Math.round(fx * rx); - int y1 = Math.round(fy * ry); - int z1 = Math.round(fz * rz); - int x2 = Math.round((fx + 1) * rx); - int y2 = Math.round((fy + 1) * ry); - int z2 = Math.round((fz + 1) * rz); - int x3 = Math.round((fx + 2) * rx); - int y3 = Math.round((fy + 2) * ry); - int z3 = Math.round((fz + 2) * rz); - double px = IrisInterpolation.rangeScale(0, 1, x1, x2, x); - double py = IrisInterpolation.rangeScale(0, 1, y1, y2, y); - double pz = IrisInterpolation.rangeScale(0, 1, z1, z2, z); - - //@builder - return getTypedSource().fromDouble(IrisInterpolation.trihermite( - getTypedSource().getDouble(x0, y0, z0), - getTypedSource().getDouble(x0, y0, z1), - getTypedSource().getDouble(x0, y0, z2), - getTypedSource().getDouble(x0, y0, z3), - getTypedSource().getDouble(x1, y0, z0), - getTypedSource().getDouble(x1, y0, z1), - getTypedSource().getDouble(x1, y0, z2), - getTypedSource().getDouble(x1, y0, z3), - getTypedSource().getDouble(x2, y0, z0), - getTypedSource().getDouble(x2, y0, z1), - getTypedSource().getDouble(x2, y0, z2), - getTypedSource().getDouble(x2, y0, z3), - getTypedSource().getDouble(x3, y0, z0), - getTypedSource().getDouble(x3, y0, z1), - getTypedSource().getDouble(x3, y0, z2), - getTypedSource().getDouble(x3, y0, z3), - getTypedSource().getDouble(x0, y1, z0), - getTypedSource().getDouble(x0, y1, z1), - getTypedSource().getDouble(x0, y1, z2), - getTypedSource().getDouble(x0, y1, z3), - getTypedSource().getDouble(x1, y1, z0), - getTypedSource().getDouble(x1, y1, z1), - getTypedSource().getDouble(x1, y1, z2), - getTypedSource().getDouble(x1, y1, z3), - getTypedSource().getDouble(x2, y1, z0), - getTypedSource().getDouble(x2, y1, z1), - getTypedSource().getDouble(x2, y1, z2), - getTypedSource().getDouble(x2, y1, z3), - getTypedSource().getDouble(x3, y1, z0), - getTypedSource().getDouble(x3, y1, z1), - getTypedSource().getDouble(x3, y1, z2), - getTypedSource().getDouble(x3, y1, z3), - getTypedSource().getDouble(x0, y2, z0), - getTypedSource().getDouble(x0, y2, z1), - getTypedSource().getDouble(x0, y2, z2), - getTypedSource().getDouble(x0, y2, z3), - getTypedSource().getDouble(x1, y2, z0), - getTypedSource().getDouble(x1, y2, z1), - getTypedSource().getDouble(x1, y2, z2), - getTypedSource().getDouble(x1, y2, z3), - getTypedSource().getDouble(x2, y2, z0), - getTypedSource().getDouble(x2, y2, z1), - getTypedSource().getDouble(x2, y2, z2), - getTypedSource().getDouble(x2, y2, z3), - getTypedSource().getDouble(x3, y2, z0), - getTypedSource().getDouble(x3, y2, z1), - getTypedSource().getDouble(x3, y2, z2), - getTypedSource().getDouble(x3, y2, z3), - getTypedSource().getDouble(x0, y3, z0), - getTypedSource().getDouble(x0, y3, z1), - getTypedSource().getDouble(x0, y3, z2), - getTypedSource().getDouble(x0, y3, z3), - getTypedSource().getDouble(x1, y3, z0), - getTypedSource().getDouble(x1, y3, z1), - getTypedSource().getDouble(x1, y3, z2), - getTypedSource().getDouble(x1, y3, z3), - getTypedSource().getDouble(x2, y3, z0), - getTypedSource().getDouble(x2, y3, z1), - getTypedSource().getDouble(x2, y3, z2), - getTypedSource().getDouble(x2, y3, z3), - getTypedSource().getDouble(x3, y3, z0), - getTypedSource().getDouble(x3, y3, z1), - getTypedSource().getDouble(x3, y3, z2), - getTypedSource().getDouble(x3, y3, z3), - px, pz, py, tension, bias)); - //@done - } - - @Override - public double toDouble(T t) { - return getTypedSource().toDouble(t); - } - - @Override - public T fromDouble(double d) { - return getTypedSource().fromDouble(d); - } - - @Override - public T get(double x, double z) { - return interpolate(x, 0, z); - } - - @Override - public T get(double x, double y, double z) { - return interpolate(x, y, z); - } -} 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 9ecbfe26f..19edb4b16 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 @@ -37,7 +37,7 @@ public class CachedStream2D extends BasicStream implements ProceduralStrea super(); this.stream = stream; this.engine = engine; - cache = new WorldCache2D<>(stream::get, size, () -> new ChunkCache2D<>("iris")); + cache = WorldCache2D.ofInts(stream::get, size, () -> new ChunkCache2D<>("iris")); IrisServices.get(PreservationRegistry.class).registerCache(this); } diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCustomBiomeSpawnTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCustomBiomeSpawnTest.java index ace98ab99..d7d8ff772 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCustomBiomeSpawnTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCustomBiomeSpawnTest.java @@ -20,8 +20,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsExplicitMatchingSpawnGroup() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("monster")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster")); assertTrue(errors.isEmpty()); } @@ -30,8 +30,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsBiomeWithoutCustomSpawns() throws Exception { File biomes = createBiomes("{\"name\":\"Plains\"}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("monster")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster")); assertTrue(errors.isEmpty()); } @@ -40,8 +40,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsMissingSpawnGroup() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("monster")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster")); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("must declare group 'MONSTER'")); @@ -51,8 +51,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsImplicitMiscSpawnGroup() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"effects\",\"spawns\":[{\"type\":\"minecraft:armor_stand\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("misc")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("misc")); assertTrue(errors.isEmpty()); } @@ -61,8 +61,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsSpawnGroupThatDisagreesWithRegistry() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MISC\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("monster")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster")); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("live entity registry requires 'MONSTER'")); @@ -72,8 +72,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsAxolotlSpawnCategory() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"cave\",\"spawns\":[{\"type\":\"minecraft:axolotl\",\"group\":\"AXOLOTLS\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("axolotls")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("axolotls")); assertTrue(errors.isEmpty()); } @@ -82,8 +82,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsUnknownSpawnEntity() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"missing:entity\",\"group\":\"MISC\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.unknown()); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.unknown()); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("unknown entity type 'missing:entity'")); @@ -93,8 +93,8 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsCaseNormalizedGroupThatRuntimeWouldNotParse() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"monster\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns( - biomes, key -> PackValidator.SpawnCategoryResolution.known("monster")); + List errors = PackSpawnValidator.validateCustomBiomeSpawns( + biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster")); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("unknown group 'monster'")); @@ -104,7 +104,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsWrongCustomDerivativeContainerType() throws Exception { File biomes = createBiomes("{\"customDerivitives\":{}}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, null); + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("customDerivitives must be an array")); @@ -114,7 +114,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsWrongSpawnContainerType() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":{}}]}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, null); + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("spawns must be an array")); @@ -124,7 +124,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsNullCustomDerivativeContainerAsAbsent() throws Exception { File biomes = createBiomes("{\"customDerivitives\":null}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, null); + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null); assertTrue(errors.isEmpty()); } @@ -133,7 +133,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsNullSpawnContainerAsAbsent() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":null}]}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, null); + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null); assertTrue(errors.isEmpty()); } @@ -142,7 +142,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void acceptsNamespacedCustomBiomeTags() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:allows_surface_slime_spawns\"]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, null); + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null); assertTrue(errors.isEmpty()); } @@ -151,7 +151,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void rejectsUnsafeCustomBiomeTags() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:../outside\"]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, null); + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("invalid tag")); @@ -161,7 +161,7 @@ public class PackValidatorCustomBiomeSpawnTest { public void convertsSpawnCategoryResolverFailureIntoBlockingError() throws Exception { File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}"); - List errors = PackValidator.validateCustomBiomeSpawns(biomes, key -> { + List errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, key -> { throw new IllegalStateException("registry unavailable"); }); diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorImportedStructurePolicyTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorImportedStructurePolicyTest.java index c32521d6b..23450a3b6 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorImportedStructurePolicyTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorImportedStructurePolicyTest.java @@ -110,18 +110,18 @@ public class PackValidatorImportedStructurePolicyTest { @Test public void explicitNullPolicyIsRejectedWhileOmissionUsesDefaults() { List missingErrors = new ArrayList<>(); - PackValidator.validateImportedStructurePolicy("overworld", new JSONObject(), missingErrors); + PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject(), missingErrors); assertTrue(missingErrors.isEmpty()); List nullErrors = new ArrayList<>(); - PackValidator.validateImportedStructurePolicy("overworld", + PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject().put("importedStructures", JSONObject.NULL), nullErrors); assertEquals(List.of("Dimension 'overworld' importedStructures must be an object."), nullErrors); } private List validate(JSONObject policy) { List errors = new ArrayList<>(); - PackValidator.validateImportedStructurePolicy("overworld", + PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject().put("importedStructures", policy), errors); return errors; } diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorLootTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorLootTest.java index 196e82f1c..50edbcca7 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorLootTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorLootTest.java @@ -28,7 +28,7 @@ public class PackValidatorLootTest { write(pack, "dimensions/main.json", "{\"loot\":{\"mode\":\"FALLBACK\",\"multiplier\":0.5," + "\"tables\":[\"global/clutter\"]}}"); - assertTrue(PackValidator.validateLootGraph(pack).isEmpty()); + assertTrue(PackLootValidator.validateLootGraph(pack).isEmpty()); } @Test @@ -37,7 +37,7 @@ public class PackValidatorLootTest { write(pack, "dimensions/main.json", "{\"loot\":{\"tables\":[\"missing\"]}}"); assertEquals(List.of("Dimension 'main'.loot.tables[0] references missing loot table 'missing'."), - PackValidator.validateLootGraph(pack)); + PackLootValidator.validateLootGraph(pack)); } @Test @@ -47,7 +47,7 @@ public class PackValidatorLootTest { + "\"loot\":[{\"type\":\"\",\"rarity\":0,\"minAmount\":3,\"maxAmount\":1," + "\"enchantments\":[{\"enchantment\":\"\",\"minLevel\":4,\"maxLevel\":2,\"chance\":2}]}]}"); - List errors = PackValidator.validateLootGraph(pack); + List errors = PackLootValidator.validateLootGraph(pack); assertTrue(errors.stream().anyMatch(error -> error.contains(".rarity must be at least 1"))); assertTrue(errors.stream().anyMatch(error -> error.contains(".minPicked must not exceed"))); @@ -63,7 +63,7 @@ public class PackValidatorLootTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "dimensions/main.json", "{\"loot\":{\"mode\":\"MERGE\",\"multiplier\":-1,\"tables\":[]}}"); - List errors = PackValidator.validateLootGraph(pack); + List errors = PackLootValidator.validateLootGraph(pack); assertEquals(2, errors.size()); assertTrue(errors.get(0).contains(".mode must be")); @@ -74,12 +74,12 @@ public class PackValidatorLootTest { public void enforcesPublishedLootMultiplierCap() throws Exception { File acceptedPack = temporaryFolder.newFolder("accepted-pack"); write(acceptedPack, "dimensions/main.json", "{\"loot\":{\"multiplier\":16,\"tables\":[]}}"); - assertTrue(PackValidator.validateLootGraph(acceptedPack).isEmpty()); + assertTrue(PackLootValidator.validateLootGraph(acceptedPack).isEmpty()); File rejectedPack = temporaryFolder.newFolder("rejected-pack"); write(rejectedPack, "dimensions/main.json", "{\"loot\":{\"multiplier\":16.01,\"tables\":[]}}"); assertEquals(List.of("Dimension 'main'.loot.multiplier must be a finite number from 0 to 16."), - PackValidator.validateLootGraph(rejectedPack)); + PackLootValidator.validateLootGraph(rejectedPack)); } @Test @@ -88,7 +88,7 @@ public class PackValidatorLootTest { write(pack, "loot/boundary.json", "{\"minPicked\":64,\"maxPicked\":64,\"maxTries\":256," + "\"loot\":[{\"type\":\"stone\",\"minAmount\":64,\"maxAmount\":64}]}"); - assertTrue(PackValidator.validateLootGraph(pack).isEmpty()); + assertTrue(PackLootValidator.validateLootGraph(pack).isEmpty()); } @Test @@ -97,7 +97,7 @@ public class PackValidatorLootTest { write(pack, "loot/excessive.json", "{\"minPicked\":65,\"maxPicked\":65,\"maxTries\":257," + "\"loot\":[{\"type\":\"stone\",\"minAmount\":65,\"maxAmount\":65}]}"); - List errors = PackValidator.validateLootGraph(pack); + List errors = PackLootValidator.validateLootGraph(pack); assertTrue(errors.contains("Loot table 'excessive'.minPicked must be at most 64.")); assertTrue(errors.contains("Loot table 'excessive'.maxPicked must be at most 64.")); diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorRemovedWorldgenFieldTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorRemovedWorldgenFieldTest.java index 007aef0c7..80705657d 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorRemovedWorldgenFieldTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorRemovedWorldgenFieldTest.java @@ -29,7 +29,7 @@ public class PackValidatorRemovedWorldgenFieldTest { "Dimension 'main' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported.", "Region 'nested/region' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported.", "Biome 'nested/biome' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported." - ), PackValidator.validateRemovedWorldgenFields(pack)); + ), PackObjectSurfaceValidator.validateRemovedWorldgenFields(pack)); } @Test diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSpawnerEntityTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSpawnerEntityTest.java index 1084a67aa..ff7cf0975 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSpawnerEntityTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSpawnerEntityTest.java @@ -110,7 +110,7 @@ public class PackValidatorSpawnerEntityTest { } private List validate(File pack) { - return PackValidator.validateSpawnerEntityReferences( + return PackSpawnValidator.validateSpawnerEntityReferences( new File(pack, "spawners"), new File(pack, "entities")); } diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureGraphTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureGraphTest.java index 32e68c3a8..44d42175e 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureGraphTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureGraphTest.java @@ -31,7 +31,7 @@ public class PackValidatorStructureGraphTest { write(pack, "jigsaw-pieces/castle/start.json", "{\"object\":\"castle/start\",\"connectors\":[{\"pool\":\"castle/end\"}]}"); write(pack, "objects/castle/start.iob", "object"); - assertTrue(PackValidator.validateStructureGraph(pack).isEmpty()); + assertTrue(PackObjectSurfaceValidator.validateStructureGraph(pack).isEmpty()); } @Test @@ -44,7 +44,7 @@ public class PackValidatorStructureGraphTest { write(pack, "structures/regional.json", "{}"); write(pack, "structures/library.json", "{}"); - assertEquals(Set.of("active", "regional"), PackValidator.collectPlacedStructureKeys(pack)); + assertEquals(Set.of("active", "regional"), PackObjectSurfaceValidator.collectPlacedStructureKeys(pack)); } @Test @@ -59,8 +59,8 @@ public class PackValidatorStructureGraphTest { + "\"ceilingPadding\":12,\"floorPadding\":2," + "\"lobeFrequency\":0.02,\"lobeStrength\":0.85}}]}"); - assertTrue(PackValidator.validateStructureGraph(pack).isEmpty()); - assertTrue(PackValidator.validateNativeStructureReplacements( + assertTrue(PackObjectSurfaceValidator.validateStructureGraph(pack).isEmpty()); + assertTrue(PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of(), Map.of()).isEmpty()); } @@ -72,7 +72,7 @@ public class PackValidatorStructureGraphTest { + "\"terrain\":{\"mode\":\"FORCE_CARVE\",\"lobeFrequency\":1.5," + "\"lobeStrength\":-0.2}}]}"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertTrue(errors.toString(), errors.stream().anyMatch( message -> message.contains("terrain.lobeFrequency must be at most 1"))); @@ -88,7 +88,7 @@ public class PackValidatorStructureGraphTest { + "\"terrain\":{\"mode\":\"FORCE_CARVE\",\"erosionStrength\":1.4," + "\"erosionFrequency\":0}}]}"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertTrue(errors.toString(), errors.stream().anyMatch( message -> message.contains("terrain.erosionStrength must be at most 1"))); @@ -103,7 +103,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeStructures\":[{\"structure\":\"minecraft:ancient_city\"}]," + "\"terrain\":{\"mode\":\"FORCE_CARVE\",\"lobeStrength\":\"strong\"}}]}"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertTrue(errors.toString(), errors.stream().anyMatch( message -> message.contains("terrain.lobeStrength must be a number"))); @@ -118,7 +118,7 @@ public class PackValidatorStructureGraphTest { + "\"jigsaw\":{\"maxDepth\":21}}]}]}"); write(pack, "structures/city.json", "{}"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertTrue(errors.toString(), errors.stream().anyMatch( message -> message.contains("exactly one non-empty backend"))); @@ -131,7 +131,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeStructures\":[{\"structure\":\"ancient_city\",\"weight\":0," + "\"jigsaw\":{\"maxDepth\":21}}]}]}"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertTrue(errors.toString(), errors.stream().anyMatch( message -> message.contains(".structure must be a namespaced registry key"))); @@ -150,7 +150,7 @@ public class PackValidatorStructureGraphTest { write(pack, "jigsaw-pieces/castle/start.json", "{\"object\":\"castle/start\",\"connectors\":[{\"pool\":\"missing-connector-pool\"}]}"); write(pack, "objects/castle/start.iob", "object"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertEquals(List.of( "Dimension 'main' structures[0].structures[0] references missing structure 'missing-structure'.", @@ -166,7 +166,7 @@ public class PackValidatorStructureGraphTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "structures/structure-index.json", "{\"counts\":{},\"structureSets\":{},\"iris\":[]}"); - assertTrue(PackValidator.validateStructureGraph(pack).isEmpty()); + assertTrue(PackObjectSurfaceValidator.validateStructureGraph(pack).isEmpty()); } @Test @@ -174,7 +174,7 @@ public class PackValidatorStructureGraphTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "structures/castle.json", "{"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertEquals(1, errors.size()); assertTrue(errors.get(0).startsWith("Structure 'castle' has invalid JSON:")); @@ -185,7 +185,7 @@ public class PackValidatorStructureGraphTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "jigsaw-pools/castle/start.json", "{"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertEquals(1, errors.size()); assertTrue(errors.get(0).startsWith("Jigsaw pool 'castle/start' has invalid JSON:")); @@ -196,7 +196,7 @@ public class PackValidatorStructureGraphTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "jigsaw-pieces/castle/start.json", "{"); - List errors = PackValidator.validateStructureGraph(pack); + List errors = PackObjectSurfaceValidator.validateStructureGraph(pack); assertEquals(1, errors.size()); assertTrue(errors.get(0).startsWith("Jigsaw piece 'castle/start' has invalid JSON:")); @@ -209,7 +209,7 @@ public class PackValidatorStructureGraphTest { assertEquals(List.of( "Jigsaw piece 'castle/start' references missing object 'castle/missing'." - ), PackValidator.validateStructureGraph(pack)); + ), PackObjectSurfaceValidator.validateStructureGraph(pack)); } @Test @@ -219,7 +219,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}"); write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\"}"); - assertTrue(PackValidator.validateNativeStructureReplacements( + assertTrue(PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, 0, 0)).isEmpty()); } @@ -230,7 +230,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeStructures\":[{\"structure\":\"minecraft:ancient_city\"}]," + "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}"); - assertTrue(PackValidator.validateNativeStructureReplacements( + assertTrue(PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of(), Map.of()).isEmpty()); } @@ -241,7 +241,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}"); write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\"}"); - List errors = PackValidator.validateNativeStructureReplacements(pack, Set.of(), Map.of()); + List errors = PackNativeStructureValidator.validateNativeStructureReplacements(pack, Set.of(), Map.of()); assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("not runtime-viable"))); assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("will not be used as a fallback"))); @@ -254,7 +254,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}"); write(pack, "structures/city.json", "{\"vanillaSource\":\"\"}"); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -1, 1)); assertTrue(errors.toString(), errors.stream().anyMatch( @@ -268,7 +268,7 @@ public class PackValidatorStructureGraphTest { + "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}"); write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\"}"); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -1, 1)); assertTrue(errors.toString(), errors.stream().anyMatch( @@ -279,7 +279,7 @@ public class PackValidatorStructureGraphTest { public void rejectsUndergroundReplacementBelowDimensionWritableRange() throws Exception { File pack = replacementPack("below", "STRUCTURE_PIECE", true, -80, -80); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4)); assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("sampled seed 0") @@ -293,7 +293,7 @@ public class PackValidatorStructureGraphTest { public void rejectsUndergroundReplacementAboveDimensionWritableRange() throws Exception { File pack = replacementPack("above", "STRUCTURE_PIECE", true, 318, 318); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4)); assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("sampled seed 0") @@ -306,7 +306,7 @@ public class PackValidatorStructureGraphTest { public void surfaceAlignsMultiPieceEnvelopeBeforeCheckingWorldBounds() throws Exception { File pack = replacementPack("surface-aligned", "CENTER_HEIGHT", false, 290, 290); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 2, -30, 10)); assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("surface-aligned") @@ -318,7 +318,7 @@ public class PackValidatorStructureGraphTest { public void doesNotApplyExactYEnvelopeGateToSingleSurfacePiece() throws Exception { File pack = replacementPack("surface-single", "CENTER_HEIGHT", false, -63, 319); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -500, 500)); assertTrue(errors.toString(), errors.isEmpty()); @@ -328,7 +328,7 @@ public class PackValidatorStructureGraphTest { public void validatesEverySurfaceExactYAnchorAgainstWritableWorldBounds() throws Exception { File pack = replacementPack("surface-exact-range", "STRUCTURE_PIECE", false, -63, 319); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4)); assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("sampled seed 0") @@ -341,7 +341,7 @@ public class PackValidatorStructureGraphTest { public void acceptsSurfaceExactYWhenEveryConfiguredAnchorFits() throws Exception { File pack = replacementPack("surface-exact-safe", "STRUCTURE_PIECE", false, -58, 315); - List errors = PackValidator.validateNativeStructureReplacements( + List errors = PackNativeStructureValidator.validateNativeStructureReplacements( pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4)); assertTrue(errors.toString(), errors.isEmpty()); diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTransformTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTransformTest.java index e4423b50a..17d547bf0 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTransformTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorStructureTransformTest.java @@ -25,7 +25,7 @@ public class PackValidatorStructureTransformTest { write(pack, "regions/nested/region.json", "{\"structures\":[{\"structures\":[\"test\"],\"translate\":{}}]}"); write(pack, "biomes/nested/biome.json", "{\"structures\":[{\"structures\":[\"test\"],\"scale\":{}}]}"); - List errors = PackValidator.validateUnsupportedStructureTransforms(pack); + List errors = PackObjectSurfaceValidator.validateUnsupportedStructureTransforms(pack); assertEquals(List.of( "Dimension 'main' structures[0] declares unsupported field 'rotation'. Structure placement transforms are not supported; remove the field.", @@ -39,7 +39,7 @@ public class PackValidatorStructureTransformTest { File pack = temporaryFolder.newFolder("pack"); write(pack, "biomes/biome.json", "{\"objects\":[{\"rotation\":{},\"translate\":{},\"scale\":{}}],\"structures\":[{\"structures\":[\"test\"]}]}"); - assertTrue(PackValidator.validateUnsupportedStructureTransforms(pack).isEmpty()); + assertTrue(PackObjectSurfaceValidator.validateUnsupportedStructureTransforms(pack).isEmpty()); } @Test diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSurfaceSupportTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSurfaceSupportTest.java index a2caf6dea..e840f9521 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSurfaceSupportTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorSurfaceSupportTest.java @@ -26,7 +26,7 @@ public class PackValidatorSurfaceSupportTest { "{\"objects\":[{\"place\":[\"a\"],\"surfaceSupportBuffer\":16,\"surfaceSupportDepth\":1," + "\"requireSurfaceSupport\":false}]}"); - assertEquals(List.of(), PackValidator.validateObjectSurfaceSupport(pack)); + assertEquals(List.of(), PackObjectSurfaceValidator.validateObjectSurfaceSupport(pack)); } @Test @@ -38,7 +38,7 @@ public class PackValidatorSurfaceSupportTest { "{\"objects\":[{\"place\":[\"a\"],\"surfaceSupportBuffer\":-1,\"surfaceSupportDepth\":0," + "\"requireSurfaceSupport\":1}]}"); - List errors = PackValidator.validateObjectSurfaceSupport(pack); + List errors = PackObjectSurfaceValidator.validateObjectSurfaceSupport(pack); assertTrue(errors.contains("Dimension 'main'.objectSurfaceSupportBuffer must be at most 16.")); assertTrue(errors.contains("Dimension 'main'.requireObjectSurfaceSupport must be a boolean.")); @@ -56,7 +56,7 @@ public class PackValidatorSurfaceSupportTest { assertEquals(List.of( "Region 'forests'.objects[0] declares removed field 'surfaceOpeningClearance'. " + "Use surfaceSupportBuffer instead." - ), PackValidator.validateObjectSurfaceSupport(pack)); + ), PackObjectSurfaceValidator.validateObjectSurfaceSupport(pack)); } @Test diff --git a/core/src/test/java/art/arcane/iris/core/project/IrisProjectEntityDependencyTest.java b/core/src/test/java/art/arcane/iris/core/project/IrisProjectEntityDependencyTest.java index 09a3af8f3..cb42ffd39 100644 --- a/core/src/test/java/art/arcane/iris/core/project/IrisProjectEntityDependencyTest.java +++ b/core/src/test/java/art/arcane/iris/core/project/IrisProjectEntityDependencyTest.java @@ -17,7 +17,7 @@ public class IrisProjectEntityDependencyTest { KSet spawners = new KSet<>(); spawners.add(spawner); - KSet entityKeys = IrisProject.collectSpawnerEntityKeys(spawners); + KSet entityKeys = IrisPackageCompiler.collectSpawnerEntityKeys(spawners); assertEquals(2, entityKeys.size()); assertTrue(entityKeys.contains("standard/passive/cow")); @@ -26,7 +26,7 @@ public class IrisProjectEntityDependencyTest { @Test public void returnsNoEntityDependenciesForEmptySpawnerSet() { - KSet entityKeys = IrisProject.collectSpawnerEntityKeys(new KSet<>()); + KSet entityKeys = IrisPackageCompiler.collectSpawnerEntityKeys(new KSet<>()); assertTrue(entityKeys.isEmpty()); } diff --git a/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java index 56214c276..d8ba1a5b7 100644 --- a/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java +++ b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderParityTest.java @@ -50,6 +50,7 @@ import art.arcane.iris.spi.PlatformItem; import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.spi.PlatformScheduler; import art.arcane.iris.spi.PlatformStructureHooks; +import art.arcane.iris.spi.PlatformWorld; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.json.JSONArray; @@ -502,7 +503,7 @@ public class SchemaBuilderParityTest { } @Override - public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) { + public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) { return false; } diff --git a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompilerTest.java b/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompilerTest.java deleted file mode 100644 index 7e1132082..000000000 --- a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioCompilerTest.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import art.arcane.iris.core.structure.authoring.StructureBackend; -import art.arcane.iris.core.structure.authoring.StructureCapability; -import art.arcane.iris.core.structure.authoring.StructureKey; -import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest; -import art.arcane.iris.core.structure.authoring.StructureResourceBundle; -import art.arcane.iris.engine.object.IrisObject; -import art.arcane.iris.engine.object.ObjectPlaceMode; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import org.junit.Test; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -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 SimpleStructureStudioCompilerTest { - @Test - public void publishIdentityMustMatchTheRuntimeResourceKey() { - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioPublishConfig( - new StructureKey("iris", "studio/owned"), - "studio/different", - 7, - 8, - ObjectPlaceMode.STRUCTURE_PIECE - )); - } - - @Test - public void compilesDeterministicOwnedGraphWithExactConnectorGeometry() throws IOException { - CompilerFixture fixture = fixture(); - - StructureResourceBundle first = SimpleStructureStudioCompiler.compile( - fixture.draft(), - fixture.config(), - fixture.objects() - ); - StructureResourceBundle second = SimpleStructureStudioCompiler.compile( - fixture.draft(), - fixture.config(), - fixture.objects() - ); - - assertEquals(StructureBackend.IRIS_ASSEMBLY, first.backend()); - assertEquals(new StructureKey("iris", "studio/hall"), first.key()); - assertTrue(first.capabilities().contains(StructureCapability.CONNECTORS)); - assertTrue(first.capabilities().contains(StructureCapability.IRIS_PLACEMENT)); - assertEquals(12, first.resources().size()); - assertEquals(resourceHashes(first), resourceHashes(second)); - - StructureOwnershipManifest manifest = StructureOwnershipManifest.from(first); - assertEquals(first.key(), manifest.structure()); - assertEquals(resourceHashes(first), manifest.resourceHashes()); - - JsonObject startPiece = json(first, "jigsaw-pieces/studio/hall/cells/0-0/start.json"); - assertFalse(startPiece.get("rotatable").getAsBoolean()); - JsonArray startConnectors = startPiece.getAsJsonArray("connectors"); - assertEquals(1, startConnectors.size()); - assertConnector(startConnectors.get(0).getAsJsonObject(), 3, 2, 0, "NORTH_NEGATIVE_Z"); - - JsonObject mainPiece = json(first, "jigsaw-pieces/studio/hall/cells/1-0/cross.json"); - assertTrue(mainPiece.get("rotatable").getAsBoolean()); - JsonArray mainConnectors = mainPiece.getAsJsonArray("connectors"); - assertEquals(4, mainConnectors.size()); - assertConnector(mainConnectors.get(0).getAsJsonObject(), 3, 2, 0, "NORTH_NEGATIVE_Z"); - assertConnector(mainConnectors.get(1).getAsJsonObject(), 5, 2, 4, "EAST_POSITIVE_X"); - assertConnector(mainConnectors.get(2).getAsJsonObject(), 3, 2, 7, "SOUTH_POSITIVE_Z"); - assertConnector(mainConnectors.get(3).getAsJsonObject(), 0, 2, 4, "WEST_NEGATIVE_X"); - - JsonObject mainPool = json(first, "jigsaw-pools/studio/hall/main.json"); - assertEquals("studio/hall/terminal", mainPool.get("fallback").getAsString()); - assertEquals(3, mainPool.getAsJsonArray("pieces").get(0).getAsJsonObject().get("weight").getAsInt()); - assertEquals(5, mainPool.getAsJsonArray("pieces").get(1).getAsJsonObject().get("weight").getAsInt()); - JsonObject startPool = json(first, "jigsaw-pools/studio/hall/start.json"); - assertEquals(1, startPool.getAsJsonArray("pieces").size()); - JsonObject terminalPool = json(first, "jigsaw-pools/studio/hall/terminal.json"); - assertEquals(1, terminalPool.getAsJsonArray("pieces").size()); - - JsonObject structure = json(first, "structures/studio/hall.json"); - assertEquals("studio/hall/start", structure.get("startPool").getAsString()); - assertEquals(1, structure.get("maxDepth").getAsInt()); - assertEquals(4, structure.get("maxSizeChunks").getAsInt()); - assertEquals("STRUCTURE_PIECE", structure.get("placeMode").getAsString()); - } - - @Test - public void failsClosedForHalfTurnOnlyPieces() { - CompilerFixture fixture = fixture(); - SimpleStructureStudioCell halfTurnMain = fixture.draft().cellOrEmpty(1, 0) - .withRotationPolicy(SimpleStructureStudioRotationPolicy.HALF_TURNS); - SimpleStructureStudioDraft unsupported = fixture.draft().withCell(halfTurnMain); - - IllegalStateException failure = assertThrows( - IllegalStateException.class, - () -> SimpleStructureStudioCompiler.compile(unsupported, fixture.config(), fixture.objects()) - ); - - assertTrue(failure.getMessage().contains("HALF_TURNS")); - } - - @Test - public void rejectsMissingUnexpectedAndWrongSizedResolvedObjects() { - CompilerFixture fixture = fixture(); - LinkedHashMap missing = new LinkedHashMap<>(fixture.objects()); - missing.remove(new SimpleStructureStudioVariantKey(1, 0, "cross")); - - IllegalStateException missingFailure = assertThrows( - IllegalStateException.class, - () -> SimpleStructureStudioCompiler.compile(fixture.draft(), fixture.config(), missing) - ); - assertTrue(missingFailure.getMessage().contains("missing=[1,0:cross]")); - - LinkedHashMap unexpected = new LinkedHashMap<>(fixture.objects()); - unexpected.put(new SimpleStructureStudioVariantKey(3, 0, "stale"), object()); - IllegalStateException unexpectedFailure = assertThrows( - IllegalStateException.class, - () -> SimpleStructureStudioCompiler.compile(fixture.draft(), fixture.config(), unexpected) - ); - assertTrue(unexpectedFailure.getMessage().contains("unexpected=[3,0:stale]")); - - LinkedHashMap wrongSize = new LinkedHashMap<>(fixture.objects()); - wrongSize.put(new SimpleStructureStudioVariantKey(2, 0, "cap"), new IrisObject(5, 4, 8)); - IllegalStateException sizeFailure = assertThrows( - IllegalStateException.class, - () -> SimpleStructureStudioCompiler.compile(fixture.draft(), fixture.config(), wrongSize) - ); - assertTrue(sizeFailure.getMessage().contains("expected 6x4x8")); - } - - @Test - public void rejectsDisconnectedPoolChannelsBeforeWritingResources() { - CompilerFixture fixture = fixture(); - SimpleStructureStudioCell wrongChannel = fixture.draft().cellOrEmpty(2, 0) - .withConnector("iris:other", 2); - SimpleStructureStudioDraft disconnected = fixture.draft().withCell(wrongChannel); - - IllegalStateException failure = assertThrows( - IllegalStateException.class, - () -> SimpleStructureStudioCompiler.compile(disconnected, fixture.config(), fixture.objects()) - ); - - assertTrue(failure.getMessage().contains("must cover the same connector channels")); - } - - private CompilerFixture fixture() { - SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(3, 1, 6, 8, 4); - SimpleStructureStudioCell start = SimpleStructureStudioCell - .create(0, 0, SimpleStructureStudioTopology.START) - .withRotationPolicy(SimpleStructureStudioRotationPolicy.FIXED) - .withConnector("iris:path", 2) - .addVariant(new SimpleStructureStudioVariant("start", 1)); - SimpleStructureStudioCell main = SimpleStructureStudioCell - .create(1, 0, SimpleStructureStudioTopology.CROSS) - .withQuarterTurns(1) - .withConnector("iris:path", 2) - .addVariant(new SimpleStructureStudioVariant("cross", 3)) - .addVariant(new SimpleStructureStudioVariant("mossy/cross", 5)); - SimpleStructureStudioCell terminal = SimpleStructureStudioCell - .create(2, 0, SimpleStructureStudioTopology.TERMINAL) - .withConnector("iris:path", 2) - .addVariant(new SimpleStructureStudioVariant("cap", 2)); - SimpleStructureStudioDraft draft = new SimpleStructureStudioDraft( - layout, - 773L, - List.of(terminal, main, start) - ); - SimpleStructureStudioPublishConfig config = new SimpleStructureStudioPublishConfig( - new StructureKey("iris", "studio/hall"), - "studio/hall", - 1, - 4, - ObjectPlaceMode.STRUCTURE_PIECE - ); - LinkedHashMap objects = new LinkedHashMap<>(); - objects.put(SimpleStructureStudioVariantKey.of(start, start.variants().get(0)), object()); - objects.put(SimpleStructureStudioVariantKey.of(main, main.variants().get(0)), object()); - objects.put(SimpleStructureStudioVariantKey.of(main, main.variants().get(1)), object()); - objects.put(SimpleStructureStudioVariantKey.of(terminal, terminal.variants().get(0)), object()); - return new CompilerFixture(draft, config, objects); - } - - private IrisObject object() { - return new IrisObject(6, 4, 8); - } - - private Map resourceHashes(StructureResourceBundle bundle) { - TreeMap hashes = new TreeMap<>(); - for (StructureResourceBundle.Resource resource : bundle.resources().values()) { - hashes.put(resource.relativePath(), resource.contentHash()); - } - return hashes; - } - - private JsonObject json(StructureResourceBundle bundle, String relativePath) { - StructureResourceBundle.Resource resource = bundle.resources().get(relativePath); - return JsonParser.parseString(new String(resource.content(), StandardCharsets.UTF_8)).getAsJsonObject(); - } - - private void assertConnector( - JsonObject connector, - int x, - int y, - int z, - String direction - ) { - JsonObject position = connector.getAsJsonObject("position"); - assertEquals(x, position.get("x").getAsInt()); - assertEquals(y, position.get("y").getAsInt()); - assertEquals(z, position.get("z").getAsInt()); - assertEquals(direction, connector.get("direction").getAsString()); - assertEquals("studio/hall/main", connector.get("pool").getAsString()); - assertEquals("iris:path", connector.get("name").getAsString()); - assertEquals("iris:path", connector.get("targetName").getAsString()); - assertEquals("ALIGNED", connector.get("joint").getAsString()); - } - - private record CompilerFixture( - SimpleStructureStudioDraft draft, - SimpleStructureStudioPublishConfig config, - Map objects - ) { - } -} diff --git a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioModelTest.java b/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioModelTest.java deleted file mode 100644 index d1d7842e4..000000000 --- a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioModelTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import com.google.gson.Gson; -import org.junit.Test; - -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -public class SimpleStructureStudioModelTest { - @Test - public void topologyMasksRotateAcrossCardinalDirections() { - assertEquals(0, SimpleStructureStudioTopology.EMPTY.baseConnectorMask()); - assertEquals(1, SimpleStructureStudioTopology.END.baseConnectorMask()); - assertEquals(5, SimpleStructureStudioTopology.STRAIGHT.baseConnectorMask()); - assertEquals(3, SimpleStructureStudioTopology.CORNER.baseConnectorMask()); - assertEquals(11, SimpleStructureStudioTopology.T.baseConnectorMask()); - assertEquals(15, SimpleStructureStudioTopology.CROSS.baseConnectorMask()); - - assertEquals(6, SimpleStructureStudioTopology.CORNER.connectorMask(1)); - assertEquals(12, SimpleStructureStudioTopology.CORNER.connectorMask(2)); - assertEquals(9, SimpleStructureStudioTopology.CORNER.connectorMask(3)); - assertTrue(SimpleStructureStudioTopology.CORNER.connects(SimpleStructureStudioDirection.EAST, 1)); - assertTrue(SimpleStructureStudioTopology.CORNER.connects(SimpleStructureStudioDirection.SOUTH, 1)); - assertFalse(SimpleStructureStudioTopology.CORNER.connects(SimpleStructureStudioDirection.NORTH, 1)); - } - - @Test - public void cellVariantsAreWeightedSelectableAndImmutable() { - SimpleStructureStudioCell cell = SimpleStructureStudioCell - .create(1, 2, SimpleStructureStudioTopology.CORNER) - .addVariant(new SimpleStructureStudioVariant("stone", 2)) - .addVariant(new SimpleStructureStudioVariant("mossy", 5)); - - assertEquals("stone", cell.activeVariant().orElseThrow().id()); - SimpleStructureStudioCell selected = cell.selectVariant("mossy"); - assertEquals("mossy", selected.activeVariant().orElseThrow().id()); - assertEquals("stone", selected.cycleVariant(1).activeVariant().orElseThrow().id()); - assertEquals(7, selected.setVariantWeight("mossy", 7).variants().get(1).weight()); - assertThrows(UnsupportedOperationException.class, () -> selected.variants().add( - new SimpleStructureStudioVariant("extra", 1) - )); - assertThrows(IllegalArgumentException.class, () -> cell.addVariant( - new SimpleStructureStudioVariant("stone", 1) - )); - } - - @Test - public void draftIsCanonicalAndJsonRoundTrips() { - SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(4, 3, 9, 11, 24); - SimpleStructureStudioCell later = SimpleStructureStudioCell.create( - 3, - 2, - SimpleStructureStudioTopology.TERMINAL - ); - SimpleStructureStudioCell earlier = SimpleStructureStudioCell.create( - 0, - 0, - SimpleStructureStudioTopology.START - ); - SimpleStructureStudioDraft draft = new SimpleStructureStudioDraft( - layout, - 9921L, - List.of(later, earlier) - ); - - assertEquals(earlier, draft.cells().get(0)); - assertEquals(later, draft.cells().get(1)); - assertEquals(36, layout.studioWidth()); - assertEquals(33, layout.studioDepth()); - assertThrows( - IllegalStateException.class, - () -> draft.withLayout(new SimpleStructureStudioLayout(5, 3, 9, 11, 24)) - ); - - Gson gson = new Gson(); - String json = gson.toJson(draft); - SimpleStructureStudioDraft restored = gson.fromJson(json, SimpleStructureStudioDraft.class); - assertEquals(draft, restored); - } - - @Test - public void draftRejectsInvalidGeometryAndConnectorHeights() { - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioLayout(0, 1, 1, 1, 1)); - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("../piece", 1)); - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("piece.", 1)); - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("con", 1)); - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("nul", 1)); - assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("com1", 1)); - SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(2, 2, 8, 8, 4); - SimpleStructureStudioCell tooTall = SimpleStructureStudioCell - .create(0, 0, SimpleStructureStudioTopology.END) - .withConnector("iris:path", 4); - - assertThrows( - IllegalArgumentException.class, - () -> new SimpleStructureStudioDraft(layout, 0L, List.of(tooTall)) - ); - assertThrows( - IllegalArgumentException.class, - () -> new SimpleStructureStudioDraft( - layout, - 0L, - List.of( - SimpleStructureStudioCell.create(0, 0, SimpleStructureStudioTopology.END), - SimpleStructureStudioCell.create(0, 0, SimpleStructureStudioTopology.CORNER) - ) - ) - ); - } -} diff --git a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepositoryTest.java b/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepositoryTest.java deleted file mode 100644 index c12533f1c..000000000 --- a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioRepositoryTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import art.arcane.iris.core.structure.authoring.StructureKey; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.stream.Stream; - -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 SimpleStructureStudioRepositoryTest { - private static final StructureKey KEY = StructureKey.parse("iris:castle/main"); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void savesLoadsAndAtomicallyReplacesDrafts() throws Exception { - Path root = temporaryFolder.newFolder("pack").toPath(); - SimpleStructureStudioRepository repository = new SimpleStructureStudioRepository(root); - SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(3, 2, 16, 16, 12); - SimpleStructureStudioDraft first = SimpleStructureStudioDraft.empty(layout, 10L); - SimpleStructureStudioDraft second = first.withPreviewSeed(20L) - .withCell(SimpleStructureStudioCell.create(1, 1, SimpleStructureStudioTopology.CORNER)); - - assertTrue(repository.load(KEY).isEmpty()); - repository.save(KEY, first); - assertEquals(Optional.of(first), repository.load(KEY)); - repository.save(KEY, second); - assertEquals(Optional.of(second), repository.load(KEY)); - try (Stream paths = Files.list(repository.draftPath(KEY).getParent())) { - assertFalse(paths.anyMatch(path -> path.getFileName().toString().endsWith(".tmp"))); - } - } - - @Test - public void malformedDraftFailsWithItsPathAndLeavesFileUntouched() throws Exception { - Path root = temporaryFolder.newFolder("pack").toPath(); - SimpleStructureStudioRepository repository = new SimpleStructureStudioRepository(root); - Path draft = repository.draftPath(KEY); - Files.createDirectories(draft.getParent()); - Files.writeString(draft, "{", StandardCharsets.UTF_8); - - Exception failure = assertThrows(Exception.class, () -> repository.load(KEY)); - - assertTrue(failure.getMessage().contains(draft.toString())); - assertEquals("{", Files.readString(draft, StandardCharsets.UTF_8)); - } -} diff --git a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSessionTest.java b/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSessionTest.java deleted file mode 100644 index 89a4a6c19..000000000 --- a/core/src/test/java/art/arcane/iris/core/structure/studio/SimpleStructureStudioSessionTest.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure.studio; - -import org.junit.Test; - -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 SimpleStructureStudioSessionTest { - @Test - public void sessionTracksDirtySavedUndoAndRedoStates() { - SimpleStructureStudioDraft initial = emptyDraft(); - SimpleStructureStudioSession session = SimpleStructureStudioSession.open(initial, 8); - - assertFalse(session.isDirty()); - assertTrue(session.setTopology(1, 1, SimpleStructureStudioTopology.CORNER)); - assertTrue(session.isDirty()); - assertEquals(3, session.draft().cellOrEmpty(1, 1).connectorMask()); - - session.markSaved(); - assertFalse(session.isDirty()); - assertTrue(session.rotateClockwise(1, 1)); - assertEquals(6, session.draft().cellOrEmpty(1, 1).connectorMask()); - assertTrue(session.isDirty()); - - assertTrue(session.undo()); - assertFalse(session.isDirty()); - assertEquals(3, session.draft().cellOrEmpty(1, 1).connectorMask()); - assertTrue(session.redo()); - assertTrue(session.isDirty()); - assertEquals(6, session.draft().cellOrEmpty(1, 1).connectorMask()); - } - - @Test - public void resizeIsAllowedOnlyBeforeContentExists() { - SimpleStructureStudioSession session = SimpleStructureStudioSession.open(emptyDraft(), 8); - SimpleStructureStudioLayout resized = new SimpleStructureStudioLayout(6, 5, 12, 14, 28); - - assertTrue(session.resize(resized)); - assertEquals(resized, session.draft().layout()); - assertTrue(session.setTopology(0, 0, SimpleStructureStudioTopology.START)); - assertThrows( - IllegalStateException.class, - () -> session.resize(new SimpleStructureStudioLayout(7, 5, 12, 14, 28)) - ); - - assertTrue(session.clearCell(0, 0)); - assertTrue(session.resize(new SimpleStructureStudioLayout(7, 5, 12, 14, 28))); - } - - @Test - public void variantsConnectorsAndRotationPoliciesUseSharedActions() { - SimpleStructureStudioSession session = SimpleStructureStudioSession.open(emptyDraft(), 16); - session.setTopology(2, 1, SimpleStructureStudioTopology.END); - session.setConnector(2, 1, "iris:hall", 6); - session.addVariant(2, 1, new SimpleStructureStudioVariant("plain", 1)); - session.addVariant(2, 1, new SimpleStructureStudioVariant("ruined", 3)); - session.selectVariant(2, 1, "ruined"); - session.setVariantWeight(2, 1, "ruined", 8); - - SimpleStructureStudioCell configured = session.draft().cellOrEmpty(2, 1); - assertEquals("iris:hall", configured.connectorChannel()); - assertEquals(6, configured.connectorHeight()); - assertEquals("ruined", configured.activeVariant().orElseThrow().id()); - assertEquals(8, configured.activeVariant().orElseThrow().weight()); - - assertTrue(session.setRotationPolicy(2, 1, SimpleStructureStudioRotationPolicy.HALF_TURNS)); - assertTrue(session.rotateClockwise(2, 1)); - assertEquals(2, session.draft().cellOrEmpty(2, 1).quarterTurns()); - assertTrue(session.setRotationPolicy(2, 1, SimpleStructureStudioRotationPolicy.FIXED)); - assertEquals(0, session.draft().cellOrEmpty(2, 1).quarterTurns()); - assertFalse(session.rotateClockwise(2, 1)); - } - - @Test - public void previewSeedSequenceAndHistoryBoundAreDeterministic() { - SimpleStructureStudioSession first = SimpleStructureStudioSession.open(emptyDraft(), 3); - SimpleStructureStudioSession second = SimpleStructureStudioSession.open(emptyDraft(), 3); - - assertEquals(first.advancePreviewSeed(), second.advancePreviewSeed()); - assertEquals(first.advancePreviewSeed(), second.advancePreviewSeed()); - - first.setPreviewSeed(1L); - first.setPreviewSeed(2L); - first.setPreviewSeed(3L); - first.setPreviewSeed(4L); - assertEquals(3, first.undoDepth()); - assertTrue(first.undo()); - assertEquals(3L, first.draft().previewSeed()); - assertTrue(first.undo()); - assertEquals(2L, first.draft().previewSeed()); - assertTrue(first.undo()); - assertEquals(1L, first.draft().previewSeed()); - assertFalse(first.undo()); - - assertTrue(first.redo()); - assertTrue(first.setPreviewSeed(99L)); - assertFalse(first.canRedo()); - } - - @Test - public void newDraftRemainsDirtyUntilSaved() { - SimpleStructureStudioSession session = SimpleStructureStudioSession.createNew(emptyDraft(), 8); - - assertTrue(session.isDirty()); - session.markSaved(); - assertFalse(session.isDirty()); - assertThrows(IllegalStateException.class, () -> session.addVariant( - 0, - 0, - new SimpleStructureStudioVariant("missing", 1) - )); - } - - private SimpleStructureStudioDraft emptyDraft() { - return SimpleStructureStudioDraft.empty( - new SimpleStructureStudioLayout(4, 4, 10, 10, 20), - 12345L - ); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java index 2e9c7ac18..8083f72f3 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java @@ -3,15 +3,14 @@ package art.arcane.iris.engine; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisGenerator; import art.arcane.iris.engine.object.IrisInterpolator; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBoundsProvider; +import art.arcane.iris.util.project.interpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider; import org.junit.Test; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; @@ -82,7 +81,7 @@ public class IrisComplexGridBoundsCacheTest { Engine.class, IrisInterpolator.class, int.class, - Set.class, + IrisGenerator[].class, int.class, int.class ); @@ -94,7 +93,7 @@ public class IrisComplexGridBoundsCacheTest { Field gridBoundsCache = IrisComplex.class.getDeclaredField("gridBoundsCache"); gridBoundsCache.setAccessible(true); ThreadLocal cache = (ThreadLocal) gridBoundsCache.get(complex); - return (long) method.invoke(complex, cache.get(), null, interpolator, 0, Set.of(), x, z); + return (long) method.invoke(complex, cache.get(), null, interpolator, 0, new IrisGenerator[0], x, z); } private float unpackLow(long packed) { diff --git a/core/src/test/java/art/arcane/iris/engine/IrisEngineDataPersistenceTest.java b/core/src/test/java/art/arcane/iris/engine/IrisEngineDataPersistenceTest.java index a8a24e5f2..32434cc07 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisEngineDataPersistenceTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisEngineDataPersistenceTest.java @@ -24,14 +24,14 @@ public class IrisEngineDataPersistenceTest { IrisEngineData first = new IrisEngineData(); first.getStatistics().setVersion(10); - IrisEngine.writeEngineDataAtomically(output, first); + EngineDataStore.writeEngineDataAtomically(output, first); IrisEngineData firstRead = new Gson().fromJson(Files.readString(output.toPath()), IrisEngineData.class); assertEquals(10, firstRead.getStatistics().getVersion()); IrisEngineData replacement = new IrisEngineData(); replacement.getStatistics().setVersion(20); - IrisEngine.writeEngineDataAtomically(output, replacement); + EngineDataStore.writeEngineDataAtomically(output, replacement); IrisEngineData replacementRead = new Gson().fromJson(Files.readString(output.toPath()), IrisEngineData.class); assertEquals(20, replacementRead.getStatistics().getVersion()); @@ -41,6 +41,6 @@ public class IrisEngineDataPersistenceTest { @Test(expected = IOException.class) public void atomicWriteRejectsParentlessPath() throws Exception { - IrisEngine.writeEngineDataAtomically(new File("parentless-engine-data.json"), new IrisEngineData()); + EngineDataStore.writeEngineDataAtomically(new File("parentless-engine-data.json"), new IrisEngineData()); } } diff --git a/core/src/test/java/art/arcane/iris/engine/IrisEngineLifecycleGateTest.java b/core/src/test/java/art/arcane/iris/engine/IrisEngineLifecycleGateTest.java index 1e3b18b86..7001d412a 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisEngineLifecycleGateTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisEngineLifecycleGateTest.java @@ -10,7 +10,7 @@ import static org.junit.Assert.assertTrue; public class IrisEngineLifecycleGateTest { @Test public void incompleteBackgroundDrainBlocksResourceRelease() { - IrisEngine.BackgroundTaskDrain drain = new IrisEngine.BackgroundTaskDrain( + EngineBackgroundTasks.BackgroundTaskDrain drain = new EngineBackgroundTasks.BackgroundTaskDrain( new TimeoutException("still running"), false); assertFalse(drain.allowsResourceRelease()); @@ -18,7 +18,7 @@ public class IrisEngineLifecycleGateTest { @Test public void completedFailedTaskAllowsSafeResourceRelease() { - IrisEngine.BackgroundTaskDrain drain = new IrisEngine.BackgroundTaskDrain( + EngineBackgroundTasks.BackgroundTaskDrain drain = new EngineBackgroundTasks.BackgroundTaskDrain( new IllegalStateException("completed exceptionally"), true); assertTrue(drain.allowsResourceRelease()); diff --git a/core/src/test/java/art/arcane/iris/engine/IrisEnginePlatformHookIsolationTest.java b/core/src/test/java/art/arcane/iris/engine/IrisEnginePlatformHookIsolationTest.java index b0b7ba0e4..b6b0f2edf 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisEnginePlatformHookIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisEnginePlatformHookIsolationTest.java @@ -49,6 +49,13 @@ public class IrisEnginePlatformHookIsolationTest { @Test public void sharedGeneratorHotPathsDoNotLinkBukkitImplementations() throws IOException { assertNoClassLinks(IrisEngine.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineBackgroundTasks.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineDataStore.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineHotloader.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineMetricsReport.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineRuntimeBuilder.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineShutdownSequence.class, BUKKIT_ENGINE_CLASSES); + assertNoClassLinks(EngineTickRegistry.class, BUKKIT_ENGINE_CLASSES); assertNoClassLinks(Engine.class, ENGINE_POLICY_CLASSES); assertNoClassLinks(EngineMode.class, PLATFORM_POLICY_CLASSES); assertNoClassLinks(EngineMantle.class, PLATFORM_POLICY_CLASSES); diff --git a/core/src/test/java/art/arcane/iris/engine/IrisWorldManagerMarkerTest.java b/core/src/test/java/art/arcane/iris/engine/IrisWorldManagerMarkerTest.java index abaf4b4ae..3f453b72f 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisWorldManagerMarkerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisWorldManagerMarkerTest.java @@ -15,26 +15,26 @@ import static org.junit.Assert.assertTrue; public class IrisWorldManagerMarkerTest { @Test public void worldBlockHeightIsTranslatedToMantleHeight() { - assertEquals(64, IrisWorldManager.toMantleY(0, -64)); - assertEquals(319, IrisWorldManager.toMantleY(255, -64)); - assertEquals(42, IrisWorldManager.toMantleY(42, 0)); + assertEquals(64, WorldBlockDropRouter.toMantleY(0, -64)); + assertEquals(319, WorldBlockDropRouter.toMantleY(255, -64)); + assertEquals(42, WorldBlockDropRouter.toMantleY(42, 0)); } @Test public void mantleHeightIsTranslatedToWorldBlockHeight() { - assertEquals(0, IrisWorldManager.toWorldY(64, -64)); - assertEquals(255, IrisWorldManager.toWorldY(319, -64)); - assertEquals(42, IrisWorldManager.toWorldY(42, 0)); + assertEquals(0, WorldBlockDropRouter.toWorldY(64, -64)); + assertEquals(255, WorldBlockDropRouter.toWorldY(319, -64)); + assertEquals(42, WorldBlockDropRouter.toWorldY(42, 0)); } @Test public void completedEntityTasksAreAccepted() { - assertTrue(IrisWorldManager.awaitEntityTasks(new CountDownLatch(0), 0, TimeUnit.MILLISECONDS)); + assertTrue(WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(0), 0, TimeUnit.MILLISECONDS)); } @Test public void incompleteEntityTasksAreRejected() { - assertFalse(IrisWorldManager.awaitEntityTasks(new CountDownLatch(1), 0, TimeUnit.MILLISECONDS)); + assertFalse(WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(1), 0, TimeUnit.MILLISECONDS)); } @Test @@ -42,7 +42,7 @@ public class IrisWorldManagerMarkerTest { AtomicBoolean preserved = new AtomicBoolean(); Thread thread = new Thread(() -> { Thread.currentThread().interrupt(); - boolean completed = IrisWorldManager.awaitEntityTasks(new CountDownLatch(1), 1, TimeUnit.SECONDS); + boolean completed = WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(1), 1, TimeUnit.SECONDS); preserved.set(!completed && Thread.currentThread().isInterrupted()); }); @@ -57,7 +57,7 @@ public class IrisWorldManagerMarkerTest { List routed = new ArrayList<>(); List fallback = new ArrayList<>(); - IrisWorldManager.routeDrops( + WorldBlockDropRouter.routeDrops( List.of("routed", "fallback"), drop -> { if (drop.equals("routed")) { diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java index 7bd31d748..5063742f8 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java @@ -288,7 +288,7 @@ public class IrisCaveCarver3DNearParityTest { .setWaterMinDepthBelowSurface(0) .setWaterRequiresFloor(true); WriterCapture firstCapture = createWriterCapture(80); - IrisCaveCarver3D.WaterSupportPlan supportPlan = new IrisCaveCarver3D.WaterSupportPlan(); + CaveWaterSupportPlan supportPlan = new CaveWaterSupportPlan(); new IrisCaveCarver3D(engine, supportedProfile).carve( firstCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, null, surfaceHeights, null, supportPlan); int candidateCount = countLiquid(firstCapture, (byte) 1); @@ -328,7 +328,7 @@ public class IrisCaveCarver3DNearParityTest { int z = 8; slice.set(0, y & 15, z, water); slice.set(8, y & 15, z, water); - IrisCaveCarver3D.WaterSupportPlan supportPlan = new IrisCaveCarver3D.WaterSupportPlan(); + CaveWaterSupportPlan supportPlan = new CaveWaterSupportPlan(); supportPlan.add(0, y, z, water, air); supportPlan.add(8, y, z, water, air); @@ -343,7 +343,7 @@ public class IrisCaveCarver3DNearParityTest { Engine engine = createEngine(80, 70); int[] surfaceHeights = filledHeights(70); WriterCapture capture = createWriterCapture(80); - IrisCaveCarver3D.WaterSupportPlan waterSupportPlan = new IrisCaveCarver3D.WaterSupportPlan(); + CaveWaterSupportPlan waterSupportPlan = new CaveWaterSupportPlan(); IrisCaveProfile waterProfile = createWaterProfile() .setDensityThreshold(new IrisStyledRange(0.15D, 0.15D, new IrisGeneratorStyle(NoiseStyle.FLAT))) .setWaterRequiresFloor(true); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java index c311c91e8..f08eb5422 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java @@ -1,7 +1,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.util.project.interpolation.InterpolationMethod3D; -import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.interpolation.Interpolation3D; import art.arcane.iris.util.project.noise.HexJamesNoise; import art.arcane.iris.util.project.noise.HexRandomSizeNoise; import art.arcane.volmlib.util.collection.KList; @@ -55,12 +55,12 @@ public class IrisMathNoiseHotPathParityTest { assertEquals( 0.5231950552025616D, - IrisInterpolation.getNoise3D(InterpolationMethod3D.TRILINEAR, 5, 7, -3, 2.5D, 3.5D, 4.5D, provider), + Interpolation3D.getNoise3D(InterpolationMethod3D.TRILINEAR, 5, 7, -3, 2.5D, 3.5D, 4.5D, provider), 0D ); assertEquals( 0.5259208842929466D, - IrisInterpolation.getNoise3D(InterpolationMethod3D.TRICUBIC, 5, 7, -3, 2.5D, 3.5D, 4.5D, provider), + Interpolation3D.getNoise3D(InterpolationMethod3D.TRICUBIC, 5, 7, -3, 2.5D, 3.5D, 4.5D, provider), 0D ); } diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectStructurePieceAirTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectStructurePieceAirTest.java index a1a3f7aec..0bb7242b4 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectStructurePieceAirTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectStructurePieceAirTest.java @@ -8,20 +8,20 @@ import static org.junit.Assert.assertTrue; public class IrisObjectStructurePieceAirTest { @Test public void explicitAirPlacesOnlyForRawStructurePieces() { - assertTrue(IrisObject.shouldPlaceObjectBlock(true, true, false)); - assertFalse(IrisObject.shouldPlaceObjectBlock(false, true, false)); + assertTrue(IrisObjectPlacementRunner.shouldPlaceObjectBlock(true, true, false)); + assertFalse(IrisObjectPlacementRunner.shouldPlaceObjectBlock(false, true, false)); } @Test public void ordinaryObjectAirRemainsNondestructive() { - assertFalse(IrisObject.shouldPlaceObjectBlock(false, true, false)); - assertTrue(IrisObject.shouldPlaceObjectBlock(false, false, false)); + assertFalse(IrisObjectPlacementRunner.shouldPlaceObjectBlock(false, true, false)); + assertTrue(IrisObjectPlacementRunner.shouldPlaceObjectBlock(false, false, false)); } @Test public void vineReplacementRemainsRejectedForEveryPlacementMode() { - assertFalse(IrisObject.shouldPlaceObjectBlock(false, false, true)); - assertFalse(IrisObject.shouldPlaceObjectBlock(true, false, true)); - assertFalse(IrisObject.shouldPlaceObjectBlock(true, true, true)); + assertFalse(IrisObjectPlacementRunner.shouldPlaceObjectBlock(false, false, true)); + assertFalse(IrisObjectPlacementRunner.shouldPlaceObjectBlock(true, false, true)); + assertFalse(IrisObjectPlacementRunner.shouldPlaceObjectBlock(true, true, true)); } } 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 fc073028d..eacef45c4 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 @@ -7,16 +7,10 @@ import art.arcane.iris.util.project.stream.ProceduralStream; import org.bukkit.block.data.BlockData; import org.junit.Test; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -84,13 +78,9 @@ public class ChunkContextPrefillPlanTest { } @Test - public void paperCommonWorkerThreadsDisableAsyncPrefillWhenPluginLoaded() throws Exception { - assertPrefillAsyncDecision("Paper Common Worker #0", false); - } - - @Test - public void irisWorkerThreadsKeepAsyncPrefillWhenPluginLoaded() throws Exception { - assertPrefillAsyncDecision("Iris 42", true); + public void singleOrNoFillTaskPrefillsInline() { + assertFalse(ChunkContext.shouldPrefillAsync(0)); + assertFalse(ChunkContext.shouldPrefillAsync(1)); } private ChunkContext createContext( @@ -164,23 +154,4 @@ public class ChunkContextPrefillPlanTest { return new ChunkContext(32, 48, complex, true, prefillPlan, null); } - - private void assertPrefillAsyncDecision(String threadName, boolean expected) throws InterruptedException, ExecutionException, java.util.concurrent.TimeoutException { - ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> { - Thread thread = new Thread(runnable); - thread.setName(threadName); - return thread; - }); - try { - Future future = executor.submit(() -> ChunkContext.prefillAsyncEligible(Thread.currentThread().getName())); - boolean actual = future.get(10, TimeUnit.SECONDS); - if (expected) { - assertTrue(actual); - } else { - assertFalse(actual); - } - } finally { - executor.shutdownNow(); - } - } } diff --git a/docs/api/README.md b/docs/api/README.md index 0716e8bb9..4c39365e0 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -18,6 +18,9 @@ else. PlaceholderAPI keys are not a compile surface, but they are a contract an operator depends on: [placeholders.md](placeholders.md). +Writing a **mod** rather than a plugin? The Fabric, Forge and NeoForge jars carry a different surface, +`art.arcane.iris.modded.api`: [modded.md](modded.md). + Anything outside `art.arcane.iris.api` is internal. `art.arcane.iris.core.*`, `art.arcane.iris.engine.*`, `art.arcane.iris.util.*` and `art.arcane.iris.spi.*` change without notice and without a deprecation cycle. If you find yourself importing `Engine`, `IrisBiome` or @@ -29,12 +32,12 @@ notice and without a deprecation cycle. If you find yourself importing `Engine`, `art.arcane.iris.api` ships in the **Bukkit plugin jar only**. The Fabric, Forge and NeoForge mod jars contain the same generator but not this package — there is no Bukkit `World`, no -`ServicesManager` and no `Event` bus to hang it on. A mod that wants generator data uses the mod -loader's own registries. +`ServicesManager` and no `Event` bus to hang it on. -The mod jars carry a separate, unrelated surface at `art.arcane.iris.modded.api`, for supplying -custom block data to the generator from a mod. It is not covered by these documents, is absent from -the Bukkit plugin jar, and shares no types with `art.arcane.iris.api`. +The mod jars carry a separate surface instead: `art.arcane.iris.modded.api`, documented in +[modded.md](modded.md). It is where a mod detects Iris levels, drives pregeneration, reads and writes +mantle data, and registers a provider so an Iris pack can place the mod's own blocks, items and mobs. +It is absent from the Bukkit plugin jar and shares no types with `art.arcane.iris.api`. Everything in these documents assumes Paper, Purpur, Leaf, Canvas, Folia or Spigot, Minecraft 26.2, Java 25. diff --git a/docs/api/modded.md b/docs/api/modded.md new file mode 100644 index 000000000..fc88b9e15 --- /dev/null +++ b/docs/api/modded.md @@ -0,0 +1,423 @@ +# Iris on Fabric, Forge and NeoForge + +`art.arcane.iris.modded.api` is the surface a **mod** compiles against. It answers three questions: is this +level generated by Iris, how do I drive Iris from my mod, and how do I get my own blocks, items and mobs +placed by an Iris pack. + +It ships in the Fabric, Forge and NeoForge jars only. It is absent from the Bukkit plugin jar, shares no types +with `art.arcane.iris.api`, and is not covered by [terrain.md](terrain.md), [world-events.md](world-events.md) +or [tree-feller.md](tree-feller.md) — those describe the Bukkit surface, which does not exist on a mod loader. + +Everything here assumes Minecraft 26.2, Java 25, and one of Fabric, Forge or NeoForge. The mod id is +`irisworldgen` on all three. + +| What you want | Where | +|---|---| +| Detect Iris, read the engine, start a pregeneration, read/write mantle data | `IrisModdedAPI` | +| Have Iris place *your* blocks, items and mobs | `ModdedDataProvider` | +| Alias one custom key onto a fixed vanilla state, with no provider class | `IrisModdedAPI.registerCustomBlockData` | + +--- + +## Depending on Iris + +**There is no published Maven artifact for the mod jars.** No module in this repository applies +`maven-publish`, and the JitPack route documented in [README.md](README.md) resolves the Bukkit sources, not +the modded adapter. Until that changes, building from source is the only path. + +The three adapters are standalone Gradle builds — each `adapters//settings.gradle` does +`includeBuild('../..')` to substitute `art.arcane:core` and `art.arcane:spi` from the root build, which is what +keeps Loom, ForgeGradle and ModDevGradle off one plugin classpath. The root build drives them through their own +wrappers: + +```bash +./gradlew buildFabric # -> dist/Iris v [Fabric] +.jar +./gradlew buildForge # -> dist/Iris v [Forge] +.jar +./gradlew buildNeoforge # -> dist/Iris v [NeoForge] +.jar +``` + +Then compile against the jar you will actually run: + +```gradle +dependencies { + compileOnly(files('libs/Iris-fabric.jar')) +} +``` + +The adapters are **not** in the root `settings.gradle` by default. Add `-PincludeModdedAdapters=true` for IDE +import only; it closes a composite build cycle (root -> adapter -> root) and Gradle may reject it. + +### Soft dependency + +Declare the optional relationship, then do not rely on load order — the ServiceLoader path below works +regardless of it, because Iris does the discovering. + +Fabric (`fabric.mod.json`) — `suggests`, not `depends`; a hard `depends` makes Iris mandatory: + +```json +{ + "suggests": { "irisworldgen": "*" } +} +``` + +NeoForge (`META-INF/neoforge.mods.toml`): + +```toml +[[dependencies.yourmod]] +modId = "irisworldgen" +type = "optional" +ordering = "AFTER" +side = "BOTH" +``` + +Forge (`META-INF/mods.toml`) is the same with `mandatory = false` instead of `type`. + +### Detecting Iris + +Two checks, and they answer different questions. + +**Is the mod present?** Ask the loader — `FabricLoader.getInstance().isModLoaded("irisworldgen")`, or +`ModList.get().isLoaded("irisworldgen")` on Forge and NeoForge. Cheap, but tells you nothing about whether +Iris actually generates anything. + +**Are Iris classes on the classpath?** If your integration lives in a class that imports +`art.arcane.iris.modded.api.*`, that class must not load unless Iris is present. Keep the imports behind a +presence check and a separate class, or probe reflectively: + +```java +private static final boolean IRIS_PRESENT = irisPresent(); + +private static boolean irisPresent() { + try { + Class.forName("art.arcane.iris.modded.api.IrisModdedAPI"); + return true; + } catch (Throwable absent) { + return false; + } +} +``` + +Do not gate on a version string. Probe for the class or method you need. + +**Is *this* level Iris?** `IrisModdedAPI.isIrisLevel(level)`. A server can mix Iris and vanilla dimensions +freely, so presence of the mod is not presence of an Iris world. + +--- + +## `IrisModdedAPI` + +All static, all null-tolerant: a null or non-Iris `ServerLevel` yields `false`, `null` or a no-op. You never +have to pre-check. + +| Method | What it does | +|---|---| +| `isIrisLevel(ServerLevel)` | Whether the level's chunk generator is `IrisModdedChunkGenerator`. The cheapest check | +| `isStudioLevel(ServerLevel)` | Whether it is a throwaway pack-authoring world. Persist nothing against one | +| `getEngine(ServerLevel)` | The `Engine` behind the level, or null. See the stability warning below | +| `pregenerate(ServerLevel, int radiusBlocks)` | Starts a cached async pregeneration around the origin | +| `pregenerate(ServerLevel, int, int centerX, int centerZ, boolean sync, boolean cached)` | Same, with a centre and write mode | +| `getMantleData(ServerLevel, int x, int y, int z, Class)` | Reads Iris's per-block sidecar storage | +| `setMantleData(ServerLevel, int x, int y, int z, T)` | Writes it | +| `deleteMantleData(ServerLevel, int x, int y, int z, Class)` | Removes a value of that type | +| `retainMantleDataForSlice(Class)` | Declares a mantle type Iris must keep rather than discard | +| `registerProvider(ModdedDataProvider)` | Registers a custom content provider imperatively | +| `registerCustomBlockData(String namespace, String key, String state)` | Aliases one key onto a fixed block state | + +### `Engine` is internal + +`getEngine` returns `art.arcane.iris.engine.framework.Engine`. That type is **internal to Iris** and changes +without a deprecation cycle — as do `art.arcane.iris.core.*`, `art.arcane.iris.util.*` and +`art.arcane.iris.spi.*`. Treat the returned `Engine` as an opaque token to hand back to Iris. Every method in +the table above that needs one resolves it for you; prefer those. + +Never cache an `Engine`. A pack hotload or a level unload replaces it, and the old instance goes inert. +`getEngine` already returns null while the generator is binding and during shutdown. + +### Pregeneration + +`pregenerate` returns as soon as the job is queued. Progress goes to Iris's own logging and boss bar, not to +your caller. Only one job runs server-wide, so it returns `false` if one is already active — and `false` also +means "not an Iris level", so check `isIrisLevel` first if you need to tell those apart. Call it on the server +thread. + +`cached = true` writes an on-disk pregeneration cache so an interrupted run resumes instead of regenerating. +`sync = true` writes chunks synchronously: slower, but it bypasses the async write queue. + +### Mantle data + +The mantle is Iris's own per-block storage, independent of chunk NBT, and it is how Iris carries data between +generation stages. Three things to know: + +1. **Coordinates are world-space.** `y` is translated by the engine's minimum height internally. A `y` outside + the engine's height range reads as null and writes as a no-op — no exception. +2. **Reads never create storage; writes do.** `getMantleData` returns null when no mantle region exists for + that column yet. `setMantleData` and `deleteMantleData` create the region, which can touch disk — do not + call them per block in a tick loop on the server thread. +3. **Declare your types or lose them.** Iris discards mantle slices it does not need once a region's + generation data has served its purpose. Any type you write and expect to read back later must be declared + once during mod setup: + +```java +IrisModdedAPI.retainMantleDataForSlice(MyMarker.class); +``` + +Registration is by canonical class name, process-wide across every Iris world, and cannot be undone. All three +mantle methods throw `IllegalStateException` if the engine's mantle has already been closed. + +--- + +## `ModdedDataProvider` + +The extension point. Implement it and a pack can name `yourmod:something` in a block palette, a loot table or a +spawn entry, and Iris will ask you to resolve it. + +```java +public interface ModdedDataProvider { + String modId(); + default boolean isReady(); // default true + Collection getTypes(ModdedDataType type); + boolean isValidProvider(Identifier id, ModdedDataType type); + default ModdedBlockData getBlockData(Identifier blockId, Map state); // default null + default void processBlockPlacement(ModdedBlockPlacementContext context); // default no-op + default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId); // default null + default void init(); // default no-op +} +``` + +`ModdedDataType` is `BLOCK`, `ITEM` or `ENTITY`. Constants may be added — write a `default` arm in any switch +expression over it. + +### The contract + +`modId()` is your identity, not decoration. It de-duplicates registrations and labels every log line Iris emits +about your provider. It must be non-null and stable; returning null aborts discovery. + +`isValidProvider(id, type)` is the gate. Iris calls it before every resolution callback, on generation threads, +for every key it could not resolve itself. Keep it to a namespace comparison or a set lookup. + +`isReady()` is how a provider whose registries populate late excuses itself. Iris **skips** a provider that +returns false rather than treating it as absent, so returning false is strictly better than returning wrong +answers. + +`getTypes(type)` feeds command suggestion and pack tooling. It is not the resolution path — return an empty +collection rather than null, and do not do work here that `isValidProvider` should do. + +`getBlockData(blockId, state)` resolves a claimed block. `state` holds the `[prop=value]` pairs from the pack's +key, already parsed, possibly empty, never null. Return null to decline and Iris tries the next provider, then +falls back to air. Return `ModdedBlockData.direct(blockState)` when the state is final. + +`processBlockPlacement(context)` finishes a **deferred** placement. Return +`ModdedBlockData.deferred(placeholder)` from `getBlockData` when the real block needs a loaded level — a block +entity, neighbour state, or mod registries not reachable from a generation thread. Iris writes your placeholder +during generation and calls you back later on the server thread with the chunk loaded. Pick a placeholder with +the same shape and occlusion as the final block so terrain around it generates correctly. Only the *first* +provider claiming the identifier is called for a given position. + +`ModdedBlockPlacementContext` is an immutable record: `engine`, `level`, `position`, `blockId`, `state`, +`blockState`. `blockState` is what is currently at `position` — normally your placeholder, though a later +generation stage may have replaced it. `state` is defensively copied and unmodifiable. + +`spawnMob(...)` spawns a claimed custom entity on the server thread. Return null to decline. + +`init()` runs once, immediately after Iris accepts your provider. + +### Threading + +| Callback | Thread | Notes | +|---|---|---| +| `isValidProvider`, `getBlockData` | Generation threads, many at once | Must be fast. Must not touch world state | +| `processBlockPlacement` | Server thread, chunk loaded | Safe to write blocks and attach block entities | +| `spawnMob` | Server thread | | +| `init` | Whichever thread registered you | Mod init for ServiceLoader, your caller otherwise | +| `modId`, `isReady`, `getTypes` | Any | | + +Implementations must be thread-safe. `getBlockData` in particular is called concurrently for every unresolved +key a pack names, which during a pregeneration is a lot. + +### Registration by ServiceLoader + +Iris discovers providers with `java.util.ServiceLoader`. Ship a service file naming your implementation's +binary name; the class needs a public no-argument constructor. + +`src/main/resources/META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider`: + +``` +com.example.yourmod.iris.YourIrisProvider +``` + +One binary name per line. Nested classes use `$`, for example +`com.example.yourmod.YourMod$IrisProvider`. + +Iris loads the service with **its own** class loader: +`ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader())`. Your service +file therefore has to be visible from Iris's loader, which is the normal case on all three loaders but is the +thing to suspect first if nothing happens. Confirm with the registration log line below; if it never appears, +fall back to `registerProvider`. + +### When discovery runs + +`ModdedCustomContentRegistry.discover()` runs inside `ModdedEngineBootstrap.bootCommon(...)`, which is the very +first thing each loader's entrypoint calls — `IrisFabricBootstrap.onInitialize`, +`IrisForgeBootstrap`/`IrisNeoForgeBootstrap` construction. That is **before** the Iris chunk generator is +registered and long before any server starts. Consequences: + +- A ServiceLoader-declared provider is available before any world could resolve a block. This is the safe path. +- Discovery runs **once per process**. A second `discover()` is a no-op. +- Your `init()` must not assume a server, a level, or a fully populated game registry. Defer that work and gate + it behind `isReady()`. + +### Registration imperatively + +`IrisModdedAPI.registerProvider(provider)` works at any time and is the option if you would rather not ship a +service file, or need to build the provider from your own config. + +Ordering is the catch: Iris only consults providers registered **before** a pack resolves the block in +question, and blocks already resolved are not revisited. Register during mod setup. Registering after Iris's +own ServiceLoader pass is fine; registering after a world has generated is not. + +A second registration under a `modId()` already present is logged and ignored. `init()` runs during the call. + +### How discovery and failures are reported + +Everything below is logged under the `Iris` logger. One line per accepted provider confirms registration — +this is what to grep for when checking whether your service file was seen: + +``` +Iris registered custom content provider 'yourmod' +``` + +A duplicate `modId()` is rejected with `already registered; ignoring duplicate`. + +Iris catches throwables from every provider callback, logs them against your `modId()`, and continues with the +remaining providers — one broken provider does not stop world generation: + +``` +Iris custom content provider 'yourmod' failed resolving block yourmod:thing +Iris custom content provider 'yourmod' failed post-placement for yourmod:thing at BlockPos{...} +Iris custom content provider 'yourmod' failed spawning mob yourmod:critter +Iris custom content provider 'yourmod' failed to initialize # registerProvider path only +``` + +`init()` during ServiceLoader discovery is the one exception, and it is all-or-nothing: a throwable there +aborts the pass, restores the registry to its pre-discovery state, and rethrows. The log line names the +provider that failed, by mod id and class name: + +``` +Iris custom content provider discovery failed at provider 'yourmod' (com.example.yourmod.iris.YourIrisProvider) +``` + +If your provider's own `modId()` throws while Iris is building that message, the class name alone is logged; if +the failure happened outside any provider, it reads `the provider service loader`. A `null` provider or a null +`modId()` from the ServiceLoader fails the pass with a message naming which. + +### Static aliases, no provider class + +For the common case of "my key is really this vanilla block", skip the provider: + +```java +IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]"); +``` + +The state string uses the same syntax packs use and is parsed **immediately** — a typo is logged at startup and +the registration dropped, rather than surfacing later as missing blocks. Aliases take precedence over provider +lookups for the same key. Null arguments are ignored. + +--- + +## How pack resolution works on modded + +Paths are relative to the loader's config directory (`config/` on a normal server install). + +| Path | What it is | +|---|---| +| `config/irisworldgen/packs//` | Installed packs. A pack is valid when `dimensions/.json` exists | +| `config/irisworldgen/generated/datapack/iris/` | The generated forced datapack. Iris owns this — do not edit it | +| `config/irisworldgen/modded.json` | Mod-side config: default pack, auto-download, primary world routing | +| `config/iris/` | Engine data directory: settings and per-world engine state | + +Note the split: the engine's data folder is `config/iris`, but every modded pack path — installer, validator, +command suggestions, forced datapack, engine creation — resolves under `config/irisworldgen/packs`. Install +packs there. + +At `bootCommon`, Iris kicks off an async default-pack prefetch. If `modded.json` has +`autoDownloadDefaultPack` enabled and the configured `defaultPack` is missing, Iris downloads +`IrisDimensions/` from the `master` branch into the packs folder. If that fails it logs a pointer to +`/iris download `. A pack that is already installed is left alone. + +When a level asks for its pack, `ModdedWorldEngines.packFolder(pack)` resolves +`config/irisworldgen/packs/`. A missing pack is a hard failure with the expected absolute path printed — +Iris does not silently generate vanilla terrain in its place. + +### The forced datapack + +Iris cannot register dimension types and per-pack biomes through a mod registry, because vanilla world creation +reads them from the datapack layer. So Iris **generates a datapack** from the installed packs and injects it as +a built-in, top-priority repository source: + +- Fabric: a `PackRepository` mixin, via `FabricForcedDatapackSources` +- Forge and NeoForge: `event.addRepositorySource(ModdedForcedDatapack.repositorySource())` + +It contributes world presets, dimension types and biomes under the `irisworldgen` namespace, with ids derived +from the pack and dimension names — `irisworldgen:packs//dimensions//preset`, +`.../dimension_type`, `.../biomes/`. This is why an Iris dimension shows up in the vanilla world +creation screen as `IRIS:`. + +It is regenerated when the pack changes: a studio hotload calls `ModdedForcedDatapack.regenerate()`. If +regeneration fails and a previously published pack is still readable, Iris keeps the last known-good one and +logs the failure rather than starting with no dimension types. + +**The failure you need to recognise.** If injection did not happen for your loader — a mixin that failed to +apply, an event that never fired — Iris logs this once at startup and world creation will fail no matter how +many times you restart: + +``` +Iris forced datapack 'iris_worldgen' was never loaded by this server. +N installed pack(s) at contributed no dimension types or custom biomes. +Datapack source injection failed for this loader (mixin/event not applied), so world +creation will fail and restarting will not fix it. +``` + +That is a loader/environment problem, not a pack problem. Nothing an integrating mod does can fix it. + +### Commands worth knowing + +The modded command tree is `/iris`, aliased `/ir` and `/irs`, gated at gamemaster permission level. + +| Command | What it tells you | +|---|---| +| `/iris pack validate [pack]` | Validates every installed pack, or one. Runs on a worker thread, reports per pack, and counts unloadable packs | +| `/iris pack status [pack]` | Replays the **recorded** validation results — blocking errors and warnings per pack. Says so and returns nothing if `validate` has not run this session | +| `/iris pack cleanup [apply]` | Previews unused pack resources; `apply` deletes them | +| `/iris pack restore [apply]` | Previews a restore of pack resources; `apply` performs it | +| `/iris datapack status` | Per Iris dimension: active dimension type, its min/max/logical height, what the pack wants, and whether they match | +| `/iris datapack install` | Writes the pack's dimension type into `/datapacks/iris/data/irisworldgen/dimension_type/` as an override | +| `/iris datapack list` | Datapack URLs declared by installed pack dimensions, plus the datapacks actually present in `/datapacks/` | +| `/iris download [branch] [overwrite]` | Installs a pack from `IrisDimensions/` into the packs folder. Aliased `dl` | +| `/iris version` | Iris version and loader | + +`/iris datapack status` is the first thing to run when an Iris dimension generates at the wrong height. A +mismatch means the level's active dimension type disagrees with the pack, which happens when a world was +created before a pack's height range changed. `install` writes the override; the world still needs a restart. + +`/iris datapack ingest`, `pull` and `remove` exist but refuse on modded, with a message explaining why: the +Modrinth ingest workflow is Bukkit tooling. Native vanilla and datapack structure placement **does** work on +modded — install the datapack into `/datapacks/` and restart, and its registered structures generate. + +--- + +## What is not supported + +- **No published artifact.** Build from source, as above. There is no Maven coordinate for the modded jars and + no `maven-publish` in this build. +- **Core types are internal.** `art.arcane.iris.engine.*`, `art.arcane.iris.core.*`, + `art.arcane.iris.util.*` and `art.arcane.iris.spi.*` change without notice. `Engine`, reachable through + `getEngine` and `ModdedBlockPlacementContext.engine()`, is the one internal type this surface exposes, and it + is exposed as a token to hand back rather than an API to call. +- **No event bus.** `IrisPlatform.callEvent` is a no-op on every mod loader adapter. There is no modded + equivalent of `IrisWorldEngineEvent` or `IrisPregenerationEvent`; poll `isIrisLevel`/`getEngine` instead. +- **No `art.arcane.iris.api`.** The Bukkit terrain, world-event, pregen and tree-feller interfaces are not in + the mod jars. There is no modded terrain-query surface yet. +- **No PlaceholderAPI.** [placeholders.md](placeholders.md) is Bukkit-only. +- **Datapack ingest is Bukkit-only**, per the command note above. +- **`ModdedCustomContentRegistry`'s resolution methods are Iris internals.** They are public only because the + adapter's generation code lives in another package. Go through `IrisModdedAPI`. diff --git a/docs/mc-version-bump.md b/docs/mc-version-bump.md new file mode 100644 index 000000000..794511038 --- /dev/null +++ b/docs/mc-version-bump.md @@ -0,0 +1,168 @@ +# Minecraft Version Bump Checklist + +`gradle.properties` `minecraftVersion` is the single source of truth for the target Minecraft +version. Most build outputs derive from it. This document lists every edit required to move Iris +to a new Minecraft version, in order. + +## Source of truth + +`gradle.properties`: + +- `minecraftVersion` — target MC version (e.g. `26.2`). Drives the Bukkit plugin `api-version`, + `BuildConstants.MINECRAFT_VERSION`, the `com.mojang:minecraft` coordinate, all mod-metadata + minecraft ranges, and every dist/jar artifact name. +- `fabricLoaderVersion` — Fabric Loader version. +- `forgeVersion` — Forge version (`-`). +- `neoForgeVersion` — NeoForge version. +- `irisVersion` — bump the trailing `-` suffix to match (e.g. `4.0.0-26.2` -> `4.0.0-27.0`). + +## Ordered steps + +1. Edit `gradle.properties`: update `minecraftVersion`, `fabricLoaderVersion`, `forgeVersion`, + `neoForgeVersion`, and the `irisVersion` suffix. + +2. Edit `gradle/libs.versions.toml`: + - `spigot` — the Spigot/Paper API pin used to compile against (`-R0.1-SNAPSHOT`). + - `fabricApi-*` — the ten Fabric API module versions, if the new MC requires different + Fabric API builds. Each module is versioned independently (`+`). + The ten are `base`, `registrySync`, `resourceLoader`, `lifecycleEvents`, `commandApi`, + `eventsInteraction`, `networking`, `rendering`, `keyMapping`, `permission`. Every one of them + is bundled jar-in-jar and must be declared in `fabric.mod.json` `jars` — see step 7. + +3. Edit `core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java` (manual, structural): + - Append a new enum constant `V_("", , ::new)`. + - `packFormat` comes from https://minecraft.wiki/w/Pack_format. + - `getLatest()` returns the last enum constant, so append; do not reorder. + - Add a matching `IDataFixer` implementation under `core/src/main/java/art/arcane/iris/core/nms/datapack/` + if the datapack format changed. + +4. Register the new Bukkit NMS binding module: + - `settings.gradle` — add `include(':adapters:bukkit:nms:v__R')`. + - `build.gradle` — add the binding to the `nmsBindings` map: + `v__R: ''` (e.g. `'26.2.build.25-alpha'`). + - Create the binding sources under `adapters/bukkit/nms/v__R/`. + +5. Update loader version-range metadata (manual floors/ranges only; the `minecraft` ranges are + templated from `minecraftVersion` and need no edit): + - `adapters/fabric/src/main/resources/fabric.mod.json` — `minecraft` is `~${minecraftVersion}` + (auto). Update the `fabricloader` floor (currently `>=0.19.3`) if the loader minimum changes, + and the `jars` list if the bundled Fabric API modules change. + - `adapters/forge/src/main/resources/META-INF/mods.toml` — `minecraft` versionRange is + `[${minecraftVersion}]` (auto). Update `loaderVersion` (currently `[65,)`) and the `forge` + dependency versionRange (also `[65,)`) for the new Forge line. Both are hand-maintained. + - `adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml` — `minecraft` versionRange + is `[${minecraftVersion}]` (auto). `loaderVersion` (currently `[3,)`) is the javafml + specification version, not the NeoForge version, and rarely moves. The `neoforge` dependency + `versionRange` is **hardcoded** (currently `[26.2,)`) and is *not* templated from + `minecraftVersion` — hand-edit it on every bump or the mod will load on the wrong NeoForge + line. + +6. Re-verify the mapping-coupled files. Six files name Mojang-mapped classes, fields, and method + descriptors directly. Nothing templates them, nothing fails fast at build time if a name moved, + and a stale entry surfaces as a silent no-op or a load-time crash. Check every one against the + new MC jar. + + Access widener (Fabric) — `accessWidener v2 official`, so the names are Mojang-mapped: + + - `adapters/fabric/src/main/resources/irisworldgen.accesswidener` + - `MinecraftServer.levels` `Ljava/util/Map;` + - `MinecraftServer.executor` `Ljava/util/concurrent/Executor;` + - `MinecraftServer.storageSource` `Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;` + - `PackRepository.sources` `Ljava/util/Set;` (accessible **and** mutable) + + Verify: each field still exists with that exact descriptor. Loom fails the build on an + unresolvable AW entry, so a rename shows up as an AW error — read it, do not delete the line. + + Access transformers (Forge and NeoForge) — must stay in sync with each other and with the AW: + + - `adapters/forge/src/main/resources/META-INF/accesstransformer.cfg` + - `adapters/neoforge/src/main/resources/META-INF/accesstransformer.cfg` + - both: `public net.minecraft.server.MinecraftServer levels` / `executor` / `storageSource` + + Verify: the three ATs match the first three AW entries. Note the ATs have no `PackRepository` + entry — Forge/NeoForge reach the pack sources through their own hooks, so do not add one + without a reason. Wired via `minecraft { accessTransformer.from(...) }` (Forge) and + `neoForge { accessTransformers.from(...) }` (NeoForge). + + Mixin configs — three JSONs, eight mixin classes, all targeting Mojang-mapped members: + + - `adapters/fabric/src/main/resources/irisworldgen.mixins.json` + (package `art.arcane.iris.fabric.mixin`, `compatibilityLevel` `JAVA_25`, Fabric only) + - `BlockItemMixin` -> `BlockItem.placeBlock`, `@At("RETURN")` + - `BlockMixin` -> `Block.getDrops(...)` with a **full descriptor** + (`BlockState, ServerLevel, BlockPos, BlockEntity, Entity, ItemInstance`) — the highest-churn + entry in the repo; the parameter list changes across MC versions + - `PackRepositoryMixin` -> `PackRepository.`, `@At("RETURN")` + - `adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json` + (package `art.arcane.iris.modded.mixin`, `compatibilityLevel` `JAVA_21`, all three loaders) + - `EntityPersistenceMixin` -> `Entity.shouldBeSaved` + - `LivingEntityLootMixin` -> `LivingEntity.dropFromLootTable(ServerLevel, DamageSource, boolean)` + — full descriptor + - `MobAwarenessMixin` -> `Mob.serverAiStep`, injecting at a **field target** + (`Lnet/minecraft/world/entity/Mob;noActionTime:I`) — verify the field, not just the method + - `adapters/modded-common/src/main/resources/irisworldgen.client.mixins.json` + (package `art.arcane.iris.client.mixin`, client-only) + - `IrisWorldOpenFlowsMixin` -> `WorldOpenFlows.confirmWorldCreation` and + `WorldOpenFlows.openWorldCheckWorldStemCompatibility` + - `IrisWorldTypeEntryMixin` -> `WorldCreationUiState.WorldTypeEntry.describePreset`, plus a + `@Shadow` member — shadows break silently if the field is renamed + + The client mixin *config* lives in `modded-common/src/main/resources` but the classes live in + `adapters/client-common/src/main/java/art/arcane/iris/client/mixin/`; the modded mixin classes + live in `adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/`. All three + adapters add both shared source dirs, so one edit hits every loader. + + Registration differs per loader and each place must list the same configs: + - Fabric — `fabric.mod.json` `mixins` (all three; the client one gated on + `"environment": "client"`). + - NeoForge — `[[mixins]]` blocks in `neoforge.mods.toml` (entity + client). + - Forge — no toml entry. The jar manifest attribute `MixinConfigs` in + `adapters/forge/build.gradle` plus `--mixin.config` args on the `runClient`/`runServer` + configurations (entity + client). Adding a mixin config on Forge means editing the manifest + attribute *and* the run args. + + `injectors.defaultRequire` is `1` in all three configs, so a mixin that no longer applies + fails the run instead of degrading quietly. Treat any "mixin apply failed" line as a bump + blocker, and run both `runClient` and `runServer` per loader — client-only mixins are not + exercised by a server run. + +7. Reconcile the Fabric jar-in-jar list. `adapters/fabric/build.gradle` adds every Fabric API + module to the `jij` configuration, which the `shadowJar` copies into `META-INF/jars` with the + version stripped from the filename. The `jij` configuration is `transitive = false`, so the + bundled set is exactly the declared set, and `fabric.mod.json` `jars` must list exactly those + filenames. After changing the module list, confirm the jar agrees: + + ``` + unzip -l "dist/Iris v [Fabric] +.jar" | grep META-INF/jars + ``` + + An entry in `jars` with no matching nested jar makes the loader refuse the mod; a nested jar + missing from `jars` is dead weight the loader never mounts. + +8. Build and verify: + - `./gradlew :core:check` + - `./gradlew buildBukkit` + - `./gradlew buildFabric` + - `./gradlew buildForge` + - `./gradlew buildNeoforge` + +## Derived automatically (do not hand-edit on a version bump) + +- Bukkit plugin `api-version` — `adapters/bukkit/plugin/build.gradle` reads `minecraftVersion`. +- `BuildConstants.MINECRAFT_VERSION` — stamped by the `generateTemplates` task in + `core/build.gradle` from `minecraftVersion`; consumed by `Tasks.supportedVersions`. +- Mod-metadata `minecraft` version ranges — templated from `minecraftVersion` at `processResources`. +- Dist/jar artifact names and the `com.mojang:minecraft` coordinate — composed from + `minecraftVersion` in the build scripts. + +## Notes + +- `build.gradle`, the adapter `build.gradle` files, and `settings.gradle` carry `.getOrElse('26.2')` + defensive defaults for the version properties. `gradle.properties` always overrides them, so a + bump does not require touching those fallbacks; refresh them only if the checked-in default should + track the current release. +- The Java literal `"26.2"` intentionally remains in `DataVersion.java` (structural enum constant), + `core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java`, and + `core/src/test/java/art/arcane/iris/core/lifecycle/PaperLibBootstrapTest.java`. The test files use + MC version strings as parser fixtures, not as a version source; update them only when the version + string formats they exercise change. diff --git a/docs/release-checklist.md b/docs/release-checklist.md new file mode 100644 index 000000000..b7925185f --- /dev/null +++ b/docs/release-checklist.md @@ -0,0 +1,87 @@ +# Iris Release Checklist + +Manual release procedure. There is no release automation by design: every step below is run by +a person and verified by eye. Work top to bottom; do not skip the verify gates. + +Before starting this publication procedure, complete the +[all-platform release readiness checklist](release-readiness-checklist.md). It contains the engineering +remediation, determinism, performance, CI, and full platform-acceptance gates. This checklist starts +only after those gates produce GO or an explicitly accepted GO-WARN decision. + +Reference values below assume the current `gradle.properties`: `irisVersion=4.0.0-26.2`, +`minecraftVersion=26.2`, `fabricLoaderVersion=0.19.3`, `forgeVersion=26.2-65.0.4`, +`neoForgeVersion=26.2.0.12-beta`. For a Minecraft version bump, do `docs/mc-version-bump.md` first, +then start this checklist. + +## a. Preflight + +- [ ] Working tree clean on the exact commit you intend to tag (`git status` shows nothing to commit). +- [ ] CI is green on that commit. The `verify` job (`.github/workflows/ci.yml`) runs + core checks, Bukkit and shared modded tests, the SPI build, the deserialization probe, the modded + artifact-verifier tests, and guarded Fabric, Forge, and NeoForge artifact builds on JDK 25. + Do not release on a red or stale run. +- [ ] `MasterChangelog.MD` Iris section is coherent: one consolidated entry set, deduplicated, no + date-sliced headers, and it describes the current shipped state (not superseded intermediate work). +- [ ] Version fields correct in `gradle.properties`: `irisVersion` is the release version and its + trailing `-` suffix matches `minecraftVersion`. For a Minecraft bump, confirm every step in + `docs/mc-version-bump.md` is done (loader ranges, `DataVersion`, NMS binding). +- [ ] JDK 25 is the active toolchain locally (`java -version` reports 25). + +## b. Build + +- [ ] From the Iris project root: `./build-all.sh`. This disables local VolmLib substitution, uses the + immutable coordinate from `gradle.properties`, and serializes the all-platform build. +- [ ] `dist/` contains the four platform jars (exact names for this release): + - [ ] `Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar` (Bukkit/Paper/Purpur/Spigot/Folia plugin) + - [ ] `Iris v4.0.0-26.2 [Fabric] 26.2+0.19.3.jar` + - [ ] `Iris v4.0.0-26.2 [Forge] 26.2+65.0.4.jar` + - [ ] `Iris v4.0.0-26.2 [NeoForge] 26.2+26.2.0.12-beta.jar` + - Naming pattern: `Iris v [] [+].jar`. +- [ ] The developer SPI jar is built by the same run at `spi/build/libs/iris-spi-4.0.0-26.2.jar`. + It is the platform-API artifact for downstream developers and is not copied into `dist/`; it is + not uploaded to the mod portals (see publish). +- [ ] Each mod jar bundles Iris core, SPI, and Iris-owned shaded libraries. LZ4, OSHI, JNA, and + JNA Platform are supplied by the Minecraft 26.2 runtime and must not be bundled or relocated. + +## c. Verify (release gates) + +- [ ] `:core:check` and `:probe:deserializationProbe` passed in CI on the tag commit (a. covers this). +- [ ] Golden-hash determinism VERIFY passes on all four platforms and matches the same hash: + - [ ] Bukkit plugin: `/iris developer goldenhash world= radius= threads=` + (automatically verifies when the matching capture already exists) + - [ ] Fabric mod: `/iris goldenhash verify ` + - [ ] Forge mod: `/iris goldenhash verify ` + - [ ] NeoForge mod: `/iris goldenhash verify ` + - The hash is interchangeable across platforms: all four MUST report identical output for the same + pack and seed. Any mismatch blocks the release. +- [ ] Live modded content-mod gate: on each loader, boot the mod jar alongside a real content mod + (e.g. Create) and generate an Iris world. Confirm no load-time rejection, no class-loader crash, + and that modded blocks/items/entities author and generate. + - [ ] Fabric + content mod + - [ ] Forge + content mod + - [ ] NeoForge + content mod +- [ ] Client-mod matrix: install the mod on the client (keybind `H` toggles the pregen HUD) and confirm: + - [ ] Modded server + modded client: HUD receives pregen progress over `irisworldgen:main`. + - [ ] Modded server + vanilla client: server generates normally; vanilla client is unaffected. + - [ ] Paper (Bukkit) server + modded client: HUD receives pregen progress over vanilla plugin messaging. + - [ ] Folia smoke: plugin loads and an Iris world generates on Folia. + - [ ] Non-Iris server + modded client: client is inert, no errors. + +## d. Publish (all manual, no automation) + +- [ ] Modrinth: upload the three mod jars and the plugin jar. Tag loaders `fabric` / `forge` / + `neoforge` on the mod files; mark the environment server + client; set game version 26.2. +- [ ] CurseForge: upload the three mod jars with the matching loader tags and game version 26.2. +- [ ] Existing plugin distribution channels: publish the plugin jar + (`Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar`) where the plugin already ships. +- [ ] Sentry: add a release note / mark the release so incoming reports map to this version + (the mod version string is the Sentry release tag). +- [ ] Storepage / `listing.json` staleness review: check the listing copy for pre-4.0 content + (Bukkit-only framing, old feature lists, screenshots). Flag anything stale for update before or + right after launch. (Review only; this checklist does not change store copy.) + +## e. Post + +- [ ] Tag the release commit (`v`) and push the tag. Archive the already verified `dist/` + bundle with the release record; no tag-triggered bundle automation is configured. +- [ ] Announce the release on the community channels once the portals show the new files live. diff --git a/docs/release-readiness-checklist.md b/docs/release-readiness-checklist.md new file mode 100644 index 000000000..9aabcff9d --- /dev/null +++ b/docs/release-readiness-checklist.md @@ -0,0 +1,490 @@ +# Iris All-Platform Release Readiness Checklist + +Engineering checklist for preparing Iris for a public release on Bukkit-family servers, Fabric, +Forge, and NeoForge. Complete this checklist before running `docs/release-checklist.md`. + +The goal is to correct confirmed defects without silently changing valid pack output, public behavior, +or platform parity. A behavior change is acceptable when it fixes a documented defect, is covered by +a regression test, and is recorded in `MasterChangelog.MD`. + +The current runtime pass prioritizes isolated world creation, deterministic generation, pregeneration, +and profiling. Hotload, reload, and shutdown refinement remains in the later lifecycle gates. +Automated release builds, tagged bundles, and publishing infrastructure are deferred; public-beta work +uses manually built artifacts and focuses on plugin/mod correctness and stability. + +## Completion rules + +- [ ] Work through the sections in order. A later section does not override a failed earlier gate. +- [ ] Add a failing regression test or deterministic reproduction before each P0/P1 correctness fix. +- [ ] Run the focused test while developing, then run the full gate for the affected platform. +- [ ] Compare fixed-pack, fixed-seed golden hashes before and after every world-generation change. +- [ ] Treat an unexpected deterministic output change as a release blocker until explained. +- [ ] Keep loader-specific behavior behind the platform boundary; reusable behavior belongs in core or SPI. +- [ ] Do not add compatibility shims, temporary adapters, or swallowed failure paths. +- [ ] Preserve full stack traces for engine, lifecycle, persistence, and operator-critical failures. +- [ ] Update `MasterChangelog.MD` as operator-visible fixes become final; merge superseded entries. +- [ ] Do not publish while any required release gate is failed, pending, or waived without an explicit reason. + +## 0. Secure and freeze the release baseline + +- [ ] Rotate the GitHub credential that was embedded in the local origin URL. +- [x] Replace the local origin with a credential-free SSH or HTTPS URL. +- [ ] Regenerate the dev-server management secret before enabling the management interface. +- [ ] Select and record the exact release commit, Minecraft version, JDK, and loader versions. +- [x] Pin VolmLib to an immutable release/tag/commit rather than `master-SNAPSHOT`. +- [x] Ensure `useLocalVolmLib` and `volmLibCoordinate` propagate into every nested adapter build. +- [x] Make the manual release build disable local VolmLib substitution by default. +- [x] Capture a baseline build and test record, running these as separate invocations: + - [x] `./gradlew :core:check :spi:build :probe:deserializationProbe -PuseLocalVolmLib=false` + - [x] `./gradlew :adapters:bukkit:plugin:test --rerun-tasks -PuseLocalVolmLib=false` + - [x] `./build-all.sh` +- [x] Confirm all four baseline jars pass archive integrity checks. +- [x] Capture baseline golden hashes for the same pack, seed, radius, and thread counts on all platforms. +- [ ] Preserve a copy of the baseline performance results described in section 8. + +Gate: the source, dependencies, generated terrain baseline, and test evidence are reproducible on a +second clean checkout. + +## 1. P0 - Make concurrent generation deterministic + +- [x] Fix the reproducible order/state-dependent generation defect. Cave painting relabeled shared, + loader-cached biome objects as `CAVE`; shallow cave resolution can return the surface biome, so later + height and biome decisions changed until `IrisComplex` was rebuilt. Carving now passes explicit cave + context to surface and ceiling decorators without mutating the shared biome, preserving cave fluid + behavior. Focused isolation and decorator tests plus a 2,025-chunk warm-sequence reproducer pass. +- [x] Scope the confirmed height-bounds cache to its owning `IrisComplex`. The previous static thread-local + cache keyed entries only by grid coordinates and interpolator index, allowing another engine or a + hotloaded complex to reuse bounds from a different generator set. Focused coverage protects both + cross-complex isolation and same-complex cache reuse. +- [x] Scope the cave carver's scratch cache to each `IrisCaveCarver3D`. The warp cache was thread-local but + shared by every cave profile and keyed only by sample coordinates, so a second profile on the same + worker could reuse warp values from the first profile's noise generator. Focused same-thread coverage + now proves distinct carvers retain their own warp samples while preserving per-carver scratch reuse. +- [x] Make cross-chunk cave-wall painting independent of adjacent mantle load order. All 37 block differences + in the two focused mantle-reset diagnoses were on local chunk edges (`x=0`, `x=15`, or `z=15`), where + `IrisCarveModifier` paints the neighboring cave wall only when that neighbor's mantle chunk contains + carving data. The carving component now declares a minimal one-block radius, which schedules the full + adjacent chunk pass through the mantle radius conversion; focused coverage protects that contract. + Packaged-runtime regeneration now retains the same fixed-seed hash on every available platform. +- [x] Repeat the fixed-seed, reset-mantle GoldenHash sequence from clean startup and after a complete pregen + on every available platform. Paper 26.2-56, Fabric Loader 0.19.3, Forge 65.0.3, and NeoForge + 26.2.0.8-beta all produced the exact combined hash + `783cf831486858129a3730e93c2823b773a40af78442ba3ebe373425eb80fab4`; every strict single-thread + 2,025-chunk pregen completed with zero failures and every post-pregen verification matched. Fabric also + matched after a controlled restart. Folia 26.2 remains unavailable from its upstream build endpoint. +- [x] Explain and fix the separate 50-chunk Paper-versus-modded biome-hash difference for byte-identical + packs. All 131 differing sampled columns were exactly `minecraft:forest` versus `minecraft:plains`: + Bukkit's NMS biome source seeded the shared scatter generator from its first coordinate-derived RNG, + while modded generation seeded it from the engine biome seed. Every runtime path now passes its owning + engine explicitly, shared registrants cache by canonical engine biome seed in a bounded eight-entry + cache, and engine-less tooling preserves supplied-seed behavior. Concurrent interleaved-engine coverage + protects exact engine ownership, seed isolation, same-seed reuse, and bounded eviction. +- [x] Sample direct Bukkit/modded biome derivatives at each world column, matching Bukkit NMS resolution. + The actuator previously reused the chunk origin for every local column, so scatter selection could + differ even after both platforms used the same generator seed. Focused actuator coverage verifies all + four coordinates in a two-by-two chunk section are distinct world positions. +- [ ] Add a two-thread barrier test that generates two chunks through the same `IrisEngine` concurrently. +- [ ] Assert each generation observes its own chunk coordinates, `ChunkContext`, and generation session. +- [ ] Add a repeated parallel golden-hash test that fails on any cross-run difference. +- [x] Remove the shared mutable `chunkContext`/session state from the engine-wide `IrisContext` path. +- [x] Give each active generation thread or lease an isolated context with explicit lifetime cleanup. +- [ ] Verify maintenance, pregen, Bukkit multicore, and modded generation use the isolated context. +- [ ] Run sequential and parallel generation for the same seed and assert identical hashes. +- [ ] Run the test under high concurrency and with generation-session close/hotload activity. + +Gate: repeated concurrent generation is deterministic, context-isolated, and hash-identical to the +single-threaded result. + +## 2. P0 - Make hotload and shutdown transactional + +This section is retained for the later lifecycle refinement pass and is not part of the current public-beta +runtime gate. Controlled restarts remain in scope only for existing-world persistence verification. + +- [ ] Add a regression test: malformed dimension edit -> failed hotload -> old engine remains usable. +- [ ] Extend the test: corrected edit -> next hotload succeeds without restarting the server. +- [ ] Build candidate dimension, loader, complex, mode, mantle, and world-manager state privately. +- [ ] Validate the complete candidate before changing the live engine. +- [ ] Seal new generation and drain active leases before clearing or replacing live resources. +- [ ] Publish the validated candidate atomically, then activate the next generation session. +- [ ] Keep the previous engine state intact when candidate loading or setup fails. +- [ ] Make `setupEngine()` fail closed and propagate fatal initialization failures. +- [ ] Route `hotloadComplex()` through the same generation-session and transactional rules. +- [ ] Ensure Bukkit exclusive-control permits are released after success, failure, and interruption. +- [ ] Restructure `IrisEngine.close()` so every cleanup stage runs even when lease draining times out. +- [ ] Add startup, failed-hotload recovery, successful-hotload, close, and restart tests. + +Gate: no failed hotload can poison the live engine, admit generation into partial state, leak permits, +or skip shutdown cleanup. + +## 3. P0 - Make `.iris` packaging complete and lossless + +- [ ] Define the complete pack resource graph in one shared traversal used by Bukkit and modded Studio. +- [ ] Traverse dimensions, regions, biomes, generators, blocks, objects, entities, spawners, loot, + structures, jigsaw pools/pieces, snippets, and every other referenced registrant. +- [ ] Include objects referenced directly by regions, not only objects reached through biomes. +- [x] Include entity resources referenced only by spawner `initialSpawns` entries, alongside normal `spawns` + dependencies, with focused export dependency regression coverage. +- [ ] Fail packaging when any required resource is missing or malformed; never report partial success. +- [ ] Stop obfuscation/export from mutating loader-cached biome or placement objects. +- [ ] Give Bukkit and modded packaging the same graph, validation, and error semantics. +- [ ] Implement the modded import/unpack path or explicitly remove the unsupported claim from the UI/docs. +- [ ] Add a minimal pack fixture containing at least one resource from every supported category. +- [ ] Add Bukkit export -> import -> export round-trip tests. +- [ ] Add modded export -> import -> export round-trip tests. +- [ ] Compare normalized JSON, binary objects, dependency counts, and final resource hashes. + +Gate: a complete fixture survives round-trip packaging without missing resources, mutated source state, +or unexplained byte/content changes. + +## 4. P0 - Make Object Studio Folia-safe and atomic + +- [ ] Add a test cell that crosses multiple chunks and multiple Folia regions. +- [ ] Capture each chunk/region snapshot only on its owning region thread. +- [ ] Assemble the final `IrisObject` only after all owned snapshots complete successfully. +- [ ] Serialize the object once and reuse the bytes for hashing and persistence. +- [ ] Write to a temporary file, flush/close it, then atomically move it over the destination. +- [ ] Commit the saved hash only after the atomic move succeeds. +- [ ] Leave the prior hash and file untouched after capture, serialization, or write failure. +- [ ] Confirm a failed write is retried on the next save rather than reported as “no changes.” +- [ ] Test empty cells, unchanged cells, partial chunk availability, failure recovery, and concurrent saves. + +Gate: Object Studio performs no cross-region Bukkit access, never exposes a partial file, and can always +retry a failed save. + +## 5. P1 - Make validation and schemas trustworthy + +- [x] Split `PackValidator` into a read-only validator and an explicit cleanup command. +- [x] Make unused-resource cleanup preview changes before moving files. +- [x] Prevent restore from overwriting a newer live file without an explicit conflict decision. +- [ ] Discover nested dimensions and resources using the same key rules as `ResourceLoader`. +- [ ] Parse and validate every referenced dependency rather than checking only file existence. +- [ ] Promote malformed referenced JSON to a blocking validation error. +- [x] Validate nested spawner `spawns` and `initialSpawns` entries against same-pack entity resources, + blocking malformed containers and entries, missing files, unsafe paths, and malformed referenced JSON. +- [ ] Validate nested unknown properties where the schema disallows them. +- [ ] Preserve namespaces for non-Minecraft enchantments and potion effects in generated schemas. +- [ ] Add deliberate cross-namespace collision fixtures. +- [ ] Make the schema executor lifecycle-owned and restartable after Bukkit reload and integrated-server stop/start. +- [ ] Add validator tests for nested resources, malformed dependencies, cleanup preview, and restore conflicts. +- [ ] Add schema tests for vanilla shorthand and fully namespaced modded values. + +Gate: validation is read-only by default, rejects broken dependency graphs, accepts valid nested packs, +and schema completion never changes registry identity. + +## 6. P1 - Harden modded generation and lifecycle + +- [ ] Make generation-session teardown cancel/retry the chunk stage instead of completing an empty chunk. +- [ ] Add a test proving a sealed engine cannot persist a blank chunk. +- [ ] Bound the modded chunk-generation queue and expose queue/backpressure metrics. +- [ ] Complete or cancel every queued future during shutdown; leave no unresolved chunk pipeline. +- [ ] Stop and await maintenance work before closing the engines it can access. +- [ ] Await executor termination and report tasks that exceed the shutdown deadline. +- [ ] Add negative-min-Y tests for sea level, base height, and base-column stone/water/air spans. +- [ ] Verify custom biome cache invalidation after a successful Studio hotload. +- [ ] Make engine-data persistence synchronized and atomic. +- [ ] Make persisted statistics safe under parallel generation. +- [ ] Test dedicated-server start/stop, integrated-server start/stop/start, and world unload/reload. +- [ ] Decide and document parity for modded entity time/weather gates, awareness, and spawn effects. + +Gate: modded shutdown/hotload cannot save blank chunks, strand futures, race maintenance, or retain stale +world state across a second server lifecycle. + +## 7. P1 - Harden pregeneration, Folia, and scheduling + +- [ ] Remove direct world/chunk/IO fallback when a Folia region scheduling call fails. +- [ ] Retry, defer, or fail the operation without touching region-owned state from the wrong thread. +- [ ] Wrap pregenerator initialization and total-count calculation in the cleanup lifecycle. +- [ ] Ensure every shutdown step runs even when `generator.close()` fails. +- [ ] Clear `regionPending` and related bookkeeping on every load/generation callback failure. +- [ ] Make the pregen cache executor restartable in the same JVM. +- [x] Distinguish cancelled or aborted partial pregeneration from full completion after the generator drains. + Cancellation now reports generated, total, failed, and remaining counts without emitting a successful + `Pregen finished` summary; focused tests cover cancellation after the first chunk, normal async-close + completion, and completion with a failed chunk. +- [ ] Replace modded scheduler `CallerRunsPolicy` with explicit backpressure that cannot move async work + onto the server thread. +- [ ] Add a bounded per-tick main-thread drain budget. +- [ ] Replace full delayed-task scans with a due-time queue or equivalent bounded scheduler. +- [ ] Stress cancellation, pause/resume, failure, shutdown, and restart under Paper and Folia. +- [ ] Verify chunk tickets, regions, files, protocol sessions, and executor threads are released afterward. + +Gate: pregeneration remains thread-correct and bounded under saturation, cancellation, failure, and restart. + +## 8. Performance and regression proof + +### Current isolated smoke evidence + +This evidence validates packaged-artifact generation and establishes a profiling candidate. It is not the +final 5,000-10,000-chunk performance baseline required by this section. + +- [x] Fixed inputs: Iris seed `1337`, GoldenHash radius `22`, one hash thread, and a 352-block serial/sync + pregeneration radius covering exactly 2,025 chunks. +- [x] Fixed host: Apple M3 Max, 128 GiB RAM, Temurin 25.0.2, 8 GiB instance heap. +- [x] Paper 26.2-56: serial pregen completed 2,025/2,025 with zero failed chunks; cancellation, + pause/status/resume, cache resume, restart persistence, and untouched far-chunk generation passed. +- [x] Fabric Loader 0.19.3: sync pregen completed 2,025/2,025 with zero failed chunks and strict + `peakInFlight=1 finalLimit=1`; controls, cache resume, restart persistence, and far generation passed. +- [x] Forge 26.2-65.0.3: sync pregen completed 2,025/2,025 with zero failed chunks and strict + `peakInFlight=1 finalLimit=1`; controls, cache resume, restart persistence, and untouched far-chunk + generation passed. +- [x] NeoForge 26.2.0.8-beta: sync pregen completed 2,025/2,025 with zero failed chunks; pause/cancel, + checkpoint resume, fresh generation, and GoldenHash capture completed against the corrected pack. +- [x] GoldenHash parity/determinism: Paper, Fabric, Forge, and NeoForge all captured the exact block+biome + hash `783cf831486858129a3730e93c2823b773a40af78442ba3ebe373425eb80fab4` from the manually built + candidate artifacts. Every platform then completed a strict single-thread 2,025-chunk pregen with zero + failures and retained that hash; Fabric retained it across restart. The historical divergent hashes are + superseded by fixes for cross-complex height bounds, cross-profile cave warp, cave-boundary scheduling, + engine-owned biome generation, per-column biome sampling, and shared-biome cave relabeling. +- [x] Paper JProfiler CPU, heap, and GC snapshots captured; explicit post-run GC reduced used heap from a + sampled peak near 5.94 GiB to approximately 475 MiB, with no retained-heap leak indicated by this run. +- [x] Fabric JProfiler sampled-allocation snapshot captured; profiling overhead made that run unsuitable + for throughput comparison. +- [x] Real content-mod fixture: Fabric, Forge, and NeoForge loaded Nerospace beta.7 with Neroland Core 1.4.0 + (plus Fabric API 0.154.2 on Fabric), resolved a custom entity/item/block through Iris, generated the + exact named structure chest item, performed once-per-chunk initial spawning with zero players, replaced + the entity's death loot, generated seven custom ore blocks in the forced test area, and completed strict + synchronous 2,025/2,025 pregeneration with zero failed chunks on every loader. + +- [ ] Choose one fixed release pack, seed, world height, radius, JVM configuration, and hardware profile. +- [ ] Warm at least 256 chunks before measuring. +- [ ] Run a 5,000-10,000 chunk pregeneration baseline on Paper. +- [ ] Run the same workload on Fabric; repeat on Forge and NeoForge before final release. +- [ ] Capture JProfiler CPU, allocation, GC, retained-object, thread, and executor-queue evidence. +- [ ] Record chunks/second, total duration, p50/p95 chunk time, allocations/chunk, peak heap, and GC pause time. +- [ ] Profile nested chunk prefill parallelism before changing it. +- [ ] Profile modded block/biome buffer allocation before pooling or changing representation. +- [ ] Profile height-bound sampling, custom biome caches, mantle tasks, and pregen region-drain complexity. +- [ ] Benchmark each optimization against the unchanged baseline with the same inputs. +- [ ] Reject or revise changes that regress median throughput by more than 5% or p95 latency/allocations by + more than 10%, unless the correctness benefit and accepted tradeoff are documented. +- [ ] Confirm optimized and baseline runs produce identical golden hashes where behavior should be unchanged. + +Gate: representative generation and pregen have repeatable baselines, no unexplained regression, and no +unbounded queue, allocation, or retained-memory growth. + +## 9. CI and deterministic test infrastructure + +Automated build and release-pipeline work in this section is deferred. The current beta pass uses manual +artifacts; only correctness tests and deterministic reproducers that directly protect runtime behavior apply. + +- [x] Add `:adapters:bukkit:plugin:test` to CI. +- [x] Expand the broad classload probe across all top-level and nested core classfiles, with an exact reviewed + class and dependency-category allowlist that rejects new classes, changed dependency namespaces, + non-missing-class failures, and stale entries. +- [ ] Move the core Bukkit purity ratchet below its current 182-file ceiling. +- [ ] Give `genProbe` a repository fixture or require an explicit portable pack path. +- [ ] Add a deterministic fixed-seed Iris-world task for Fabric, Forge, and NeoForge. +- [x] Make worldcheck return a failing process result when its internal result is FAIL. +- [x] Prevent `buildAllToOut` nested builds from racing root tasks over `core/build`. +- [x] Verify nested adapter builds honor the selected VolmLib source/coordinate. +- [x] Add packaged-jar server boots; manually assembled Bukkit, Fabric, Forge, and NeoForge artifacts all + reached their runtime-ready state in isolated instances, including a real multi-mod classpath. + +Gate: a clean CI run proves tests, deterministic generation, packaging, and server startup from the actual +release artifacts. + +## 10. Full platform acceptance matrix + +Use the exact packaged release jars, not development classes. + +The current isolated smoke proves fresh non-empty generation, exact fixed-seed block-and-biome parity, and +complete serial/sync 2,025-chunk pregeneration on Paper, Fabric, Forge, and NeoForge. A second real content-mod +fixture also passes entity, item, block, structure loot, death loot, headless initial-spawn, and 2,025-chunk +pregeneration gates on all three mod loaders. It does not yet satisfy the minimum/latest loader, complete +Bukkit-family, client, lifecycle, or full pregen-control matrix below. + +- [ ] Bukkit-family server matrix: + - [ ] Paper current target + - [ ] Purpur current target + - [ ] Folia current target + - [ ] Spigot/CraftBukkit if still advertised as supported +- [ ] Mod-loader matrix: + - [ ] Fabric declared minimum loader + - [ ] Fabric latest compatible loader + - [ ] Forge declared minimum loader + - [ ] Forge latest compatible loader + - [ ] NeoForge declared minimum loader + - [ ] NeoForge latest compatible loader +- [ ] On every server target: + - [ ] Fresh Iris world creation and non-empty chunk generation + - [ ] Existing Iris world restart and new-chunk generation + - [ ] Custom biome registration and client synchronization + - [ ] Structures, objects, loot, spawners, and entities + - [ ] Golden-hash match for the shared pack/seed + - [ ] Pregeneration start, pause, resume, cancel, restart, and shutdown + - [ ] Studio validation, hotload failure recovery, and successful hotload where supported + - [ ] Clean startup and shutdown without leaked threads or incomplete futures +- [x] Content-mod gate on Fabric, Forge, and NeoForge using Nerospace beta.7 and Neroland Core 1.4.0 with + authored `nerospace:meadow_loper`, `nerospace:raw_nerosium`, and `nerospace:nerosium_ore` resources. +- [ ] Client matrix: + - [ ] Modded Iris server + Iris client mod + - [ ] Modded Iris server + client without Iris where loader rules permit + - [ ] Bukkit Iris server + Iris client mod over plugin messaging + - [ ] Non-Iris server + Iris client mod remains inert + - [ ] Integrated singleplayer create, leave, and create/join again in the same client process + - [ ] Pregen HUD, Vision map, cursor overlay, keybinds, and Studio toasts + +Gate: every advertised server, loader, client, and content path completes the same acceptance scenario or +has a clearly documented intentional capability difference. + +## 11. Documentation and repository hygiene + +- [x] Make runtime splash/version identity match the artifact version; remove stale `4.0 RC.1.1.6` text. +- [x] Correct README pregen syntax, including the required radius. +- [ ] Document how to select an Iris world preset on each mod loader. +- [ ] Distinguish automatic pack installation from automatic Iris main-world selection. +- [ ] Publish an accurate Bukkit-versus-modded Studio capability matrix. +- [ ] Document intentional entity-spawn and tooling differences that remain. +- [x] Remove tracked generated SIMD benchmark `.class` files and jar outputs. +- [x] Keep generated server worlds, caches, credentials, and build artifacts ignored. +- [ ] Consolidate the Iris section of `MasterChangelog.MD` to the final shipped behavior. +- [ ] Review store/listing copy, screenshots, commands, supported platforms, and Java requirements. +- [ ] Write release notes with upgrade instructions, known limitations, and rollback guidance. + +### Confirmed release blockers and follow-ups + +- [ ] Freeze the default overworld pack to an immutable release input. The runtime downloader currently + follows the mutable `master` branch, so any recorded tree checksum remains reproducible only while + that upstream content is unchanged. Immutable branch/tag/commit URL resolution is implemented, but + published commit `8e32852ee6ecd039fae27a36f701f57cdc02e83f` predates the five local slime-category + and biome-tag corrections, the dormant standard entity resource restoration, and removal of the + legacy default ambient-spawner attachments; publish those pack edits under a new commit/tag before + pinning automatic installs. +- [x] Make modded GoldenHash metadata use the active Iris engine seed. Fabric, Forge, and NeoForge generated + identical output from Iris seed `1337`, but filenames and headers recorded each vanilla level seed, + preventing one captured baseline file from being reused directly across loaders. +- [x] Correct the default overworld pack's slime spawn category from implicit `MISC` to explicit `MONSTER` + in `biomes/vanilla/mangrove_swamp.json`, `biomes/swamp/cambian-drift.json`, + `biomes/swamp/cambian-drift-extended.json`, `biomes/swamp/marsh.json`, and + `biomes/swamp/marsh-rotten.json`. NeoForge exposes the bad category at startup; all loaders generate + the same bad datapack entry, which can affect natural slime spawning and mob-cap accounting. +- [x] Extend `PackValidator` to reject authored custom-biome spawn categories that disagree with the live + entity category instead of allowing the bad datapack to reach loader validation. +- [x] Restore exactly the 36 standard entity resources required by the overworld's retained spawner library + from their last authored revision, preserving their type/surface values without restoring deleted + unique entities, while detaching every regional and spider-infestation ambient spawner so the library + remains dormant unless a pack author explicitly references it. +- [x] Delegate ongoing natural spawn tables in custom Iris biomes to each `vanillaDerivative` on Bukkit, + Fabric, Forge, and NeoForge; explicit custom entries replace the same native entity type and extend + the rest, while structure overrides remain authoritative and cached tables avoid hot-path allocation. +- [x] Add validated custom-biome tag opt-ins and put all five explicit overworld slime biomes in + `minecraft:allows_surface_slime_spawns`, allowing Minecraft's native surface-slime checks to succeed. +- [x] Add Minecraft 26.2 default-clock metadata to generated Iris overworld and End dimension types so + `/time set`, `/time add`, time queries, and clock controls work in Iris overworld dimensions. An + isolated Paper 26.2 runtime loaded a dimension using `iris:overworld`, reported the + `minecraft:overworld` clock, accepted day and night time markers, and returned the clock time. +- [x] Make synchronous modded pregen completion diagnostics report meaningful concurrency values. The + successful runs reported `peakInFlight=0 finalLimit=32` despite a strict `inFlightCap=1` sync mode. +- [ ] Pin or fix the isolated test harness behavior before treating it as release evidence: setting an + instance isolated currently leaves consumer-content symlinks in place. This pass used a fresh, + dedicated harness root, so those links pointed only to test-local content and did not contaminate + the test, but the isolation flag alone is insufficient. +- [x] Resolve the fixed-seed order/state-dependent block generation and Paper-versus-modded biome-hash + difference. The manually built candidate produced one exact full hash before and after 2,025-chunk + pregeneration on Paper, Fabric, Forge, and NeoForge; Fabric also retained it after restart. +- [ ] Re-run Folia when an upstream 26.2 server build becomes available. The official 26.2 build endpoint + currently returns `version_not_found`; an incompatible 26.1.2 runtime is not acceptable beta evidence. +- [ ] Nerospace beta.7's bundled `nerospace:guide/new_life` advancement uses the obsolete + `minecraft:entity_sub_predicate_type`/`minecraft:type` shape and logs one datapack parse error on Fabric, + Forge, and NeoForge 26.2. Iris's custom block, item, entity, chest loot, death loot, and pregeneration + integration all pass despite that independent content-mod error; update Nerospace before using it as a + clean-log beta recommendation. +- [x] Preserve structure-level loot through placement persistence. Newly placed structure containers receive a + versioned, delimiter-safe marker containing the piece object, deterministic placement id, and owning + structure; `Engine.getObjectPlacement()` reconstructs authored loot in order at weight 1 for the existing + Bukkit and modded application paths without overriding global loot. Legacy `object@id` markers remain + readable, malformed and unknown-version markers fail safely, and marker writes are storage-container-only. +- [x] Remove the unsupported `IrisStructurePlacement` `rotation`, `translate`, and `scale` fields from beta + authoring and generated schemas. Read-only pack validation now blocks those keys specifically inside + dimension, region, and biome `structures[]` entries instead of accepting settings with no runtime effect; + ordinary object-placement transforms remain valid and are not inspected by this check. + +Gate: documentation and distribution metadata describe the behavior users will actually receive. + +## 12. Final GO/NO-GO gate + +- [x] `unit-tests`: pass. +- [ ] `qa-validation`: pass across the full matrix. +- [ ] `edge-case-review`: pass or all remaining risks explicitly accepted. +- [ ] `perf-regression`: pass against the recorded baseline. +- [ ] `release-dry-run`: pass using final packaged artifacts. +- [ ] `changelog-ready`: pass. +- [x] `manual-smoke`: pass. +- [ ] `docs-updated`: pass. +- [ ] `known-issues-reviewed`: pass. +- [ ] Working tree is clean on the exact release commit. +- [ ] CI is green on that commit and all evidence artifacts are retained. +- [ ] Complete every item in `docs/release-checklist.md` without rebuilding from different source. + +Release decision: + +- [ ] **GO** - every required check passes and no unresolved warning remains. +- [ ] **GO-WARN** - every required check passes and each warning is documented and explicitly accepted. +- [x] **NO-GO** - any required check fails or remains pending. + +## Fixes already completed in the current working tree + +- [x] Concurrent generation binds immutable engine/session/chunk context per worker scope and restores or + removes that binding at scope close. +- [x] Context-backed stream caches reject the wrong engine, a stale generation session, and coordinates + outside the bound chunk. +- [x] Registry-backed mantle and `.mat` reads bind the owning pack data explicitly, and heightmap object + placement no longer depends on ambient generation context. +- [x] Configured Matter placements use the initialized canonical Matter loader instead of a duplicate null field. +- [x] Deterministic barrier, worker-reuse, nested-scope, close-order, and context-cache regression tests pass. +- [x] Bukkit/Paper pregeneration accepts small positive radii and a strict one-in-flight `serial=true` mode + without changing normal Paper/Folia concurrency. +- [x] Pregeneration drains the final backend callback before reporting completion, eliminating the observed + 2,024/2,025 success summary; delayed final success and failure paths have regression coverage. +- [x] Modded synchronous and asynchronous completion counters count only successful chunks, and final + summaries include generated, total, failed, and duration values. +- [x] GoldenHash null-biome fallback is explicitly `minecraft:plains` on Bukkit and modded adapters. +- [x] GoldenHash metadata uses the active Iris seed across every platform, and GitHub pack downloads accept + validated immutable commit and tag references in preparation for freezing the default pack. +- [x] Runtime splash identity derives from the packaged artifact version instead of a stale release label. +- [x] Pack validation is read-only; cleanup and restore require explicit preview/apply flows with fresh scans, + direct-child containment, conflict refusal, per-pack serialization, truthful rollback reporting, and + no-overwrite quarantine handling. +- [x] Custom-biome spawn groups validate against live platform entity categories, including `AXOLOTLS`, and + the default overworld slime records are explicitly `MONSTER` with isolated NeoForge proof. +- [x] Spawner entity dependency validation covers both runtime spawn lists, malformed entry/container shapes, + missing or malformed referenced entities, nested resource keys, and path containment; the default pack's + dormant spawner library resolves to exactly 36 standard entities and no restored unique entities. +- [x] `.iris` packaging collects entity dependencies from both normal and initial spawner lists, so an entity + used exclusively during initial chunk spawning remains present after export. +- [x] Newly placed structure containers persist versioned structure ownership and resolve the structure's authored + loot through the shared Bukkit/modded placement path without replacing global loot or consuming generation RNG. +- [x] VolmLib is pinned to commit `d9026a7c8ebc391c8109f401ce79a0ce65df3969`; local-development and + clean remote-resolution modes propagate through every nested platform build. +- [x] Headless classload validation scans all 1,166 compiled core classes, including all 353 nested classfiles; + 331 nested classes initialize without server APIs and the remaining 22 match exact reviewed class and + dependency-namespace entries. +- [x] Modded worldcheck uses a non-daemon coordinator, stops the server before exiting, and returns nonzero + for internal failure, timeout, interruption, thrown checks, and shutdown failure; its exit contract is + covered by the Fabric shared-source test gate. +- [x] Fabric protocol startup tolerates the pre-player-list server phase. +- [x] NeoForge registers the shared payload once as bidirectional. +- [x] Fabric distributable metadata declares the bundled transitive access-widener. +- [x] Fabric, Forge, and NeoForge relocate Iris's embedded Sentry runtime so another mod can bundle Sentry + without a duplicate-package module-resolution failure; the corrected Forge and NeoForge artifacts boot + alongside Neroland Core's jar-in-jar Sentry dependency. +- [x] Fabric, Forge, and NeoForge resolve Minecraft 26.2's supplied OSHI, JNA, JNA Platform, and LZ4 + implementations without embedding or relocating them. The distribution gate scans outer classes and + nested jars for private rewritten references or duplicate runtime libraries before accepting each artifact. +- [x] Headless force-loaded chunks receive structure loot and initial entity spawning on Bukkit and every mod + loader without requiring a player to enter the world. Bukkit target collection is global/region-safe, + bounded, rotating, and deduplicated; modded initial-spawn requests retry and recover without caller-runs + disk work on the server tick. +- [x] Bukkit world creation preserves explicit `pack:dimensionKey` selection through pack installation and + engine creation, matching the modded command behavior and preventing same-key cross-pack collisions. +- [x] Bukkit/Folia world-manager snapshots keep world, player, entity, chunk, and force-load API access on the + appropriate global, entity, or region scheduler and refresh saturation before its early-return gate. +- [x] Multicore Perfection waits for isolated worker completion. +- [x] Bukkit exclusive-control permits release after failures and interruptions. +- [x] Modded sea-level/base-column calculations use absolute world Y. +- [x] Low-risk map drawing, post-processing, base-column, and block-buffer loop costs were reduced. +- [x] Core tests, Bukkit plugin tests, all-platform assembly, archive integrity, and fresh Iris-world checks + on Fabric, Forge, and NeoForge passed for this fix set. + +These completed items remain subject to the final packaged-artifact, Bukkit/Folia, concurrency, and +performance gates above. diff --git a/gradle/volmlib-resolution.settings.gradle b/gradle/volmlib-resolution.settings.gradle new file mode 100644 index 000000000..3ad9c67dc --- /dev/null +++ b/gradle/volmlib-resolution.settings.gradle @@ -0,0 +1,98 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * Shared VolmLib source resolution, applied by every Iris settings file: + * + * root settings.gradle + * adapters/fabric settings.gradle + * adapters/forge settings.gradle + * adapters/neoforge settings.gradle + * + * Apply with an explicit File so the path never depends on the invocation directory: + * + * apply from: new File(settingsDir, 'gradle/volmlib-resolution.settings.gradle') // root + * apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile // adapters + * + * `settingsDir` resolves against the *including* build, so the upward search for a sibling + * VolmLib checkout works from the repo root and from a nested adapter build alike. + * + * Behaviour: + * -PuseLocalVolmLib=false disables source substitution (CI uses this). + * -PlocalVolmLibDirectory= explicit VolmLib checkout (relative paths resolve + * against the including build's settings directory). + * VOLMLIB_DIR= same, via environment. + * otherwise walk up from settingsDir looking for a `VolmLib` directory + * that contains a settings script. + * + * When nothing is found the build falls back to the published `volmLibCoordinate` from + * gradle.properties. + */ + +import java.io.File + +boolean irisHasVolmLibSettings(File directory) { + if (directory == null) { + return false + } + + new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists() +} + +File irisResolveLocalVolmLibDirectory() { + String configuredPath = providers.gradleProperty('localVolmLibDirectory') + .orElse(providers.environmentVariable('VOLMLIB_DIR')) + .orNull + if (configuredPath != null && !configuredPath.isBlank()) { + File configuredDirectory = new File(configuredPath) + if (!configuredDirectory.isAbsolute()) { + configuredDirectory = new File(settingsDir, configuredPath) + } + if (irisHasVolmLibSettings(configuredDirectory)) { + return configuredDirectory + } + } + + File currentDirectory = settingsDir + while (currentDirectory != null) { + File candidate = new File(currentDirectory, 'VolmLib') + if (irisHasVolmLibSettings(candidate)) { + return candidate + } + + currentDirectory = currentDirectory.parentFile + } + + null +} + +boolean irisUseLocalVolmLib = providers.gradleProperty('useLocalVolmLib') + .orElse('true') + .map { String value -> value.equalsIgnoreCase('true') } + .get() +File irisLocalVolmLibDirectory = irisResolveLocalVolmLibDirectory() + +if (irisUseLocalVolmLib && irisLocalVolmLibDirectory != null) { + includeBuild(irisLocalVolmLibDirectory) { + dependencySubstitution { + substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared')) + substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared')) + substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared')) + } + } +} diff --git a/listing.json b/listing.json deleted file mode 100644 index e657b976b..000000000 --- a/listing.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - "overworld IrisDimensions/overworld", - "vanilla IrisDimensions/vanilla", - "flat IrisDimensions/flat", - "redstone IrisDimensions/redstone", - "mars IrisDimensions/mars", - "example IrisDimensions/example", - "newhorizons IrisDimensions/newhorizons", - "theend IrisDimensions/theend" -] diff --git a/logs/latest.log b/logs/latest.log deleted file mode 100644 index e69de29bb..000000000 diff --git a/packignore.ignore b/packignore.ignore deleted file mode 100644 index 9bdafae07..000000000 --- a/packignore.ignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -*.code-workspace -*.txt \ No newline at end of file diff --git a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java index 1ca9b4523..d0a84d2ae 100644 --- a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java +++ b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java @@ -671,7 +671,7 @@ public final class StubPlatform implements IrisPlatform { } @Override - public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) { + public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) { return false; } diff --git a/probe/src/main/resources/classload-allowlist.tsv b/probe/src/main/resources/classload-allowlist.tsv index d96222181..5105a6fa3 100644 --- a/probe/src/main/resources/classload-allowlist.tsv +++ b/probe/src/main/resources/classload-allowlist.tsv @@ -76,11 +76,7 @@ art.arcane.iris.util.common.inventorygui.WindowResolution BUKKIT_API org.bukkit. art.arcane.iris.util.common.misc.Bindings BUKKIT_API org.bukkit.plugin.Plugin art.arcane.iris.util.common.misc.ServerProperties SERVER_RUNTIME_FILE server.properties art.arcane.iris.util.common.misc.SlimJar BUKKIT_API org.bukkit.plugin.Plugin -art.arcane.iris.util.common.plugin.CommandDummy BUKKIT_API org.bukkit.command.CommandSender -art.arcane.iris.util.common.plugin.Controller BUKKIT_API org.bukkit.event.Listener -art.arcane.iris.util.common.plugin.IController BUKKIT_API org.bukkit.event.Listener art.arcane.iris.util.common.plugin.IrisService BUKKIT_API org.bukkit.event.Listener -art.arcane.iris.util.common.plugin.RouterCommand BUKKIT_API org.bukkit.command.Command art.arcane.iris.util.common.plugin.VolmitPlugin BUKKIT_API org.bukkit.event.Listener art.arcane.iris.util.common.plugin.VolmitSender BUKKIT_API org.bukkit.command.CommandSender art.arcane.iris.util.common.plugin.chunk.ChunkTickets BUKKIT_API org.bukkit.event.Listener diff --git a/settings.gradle b/settings.gradle index 7a936f4c2..f3e4cca6b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,60 +16,42 @@ * along with this program. If not, see . */ -import java.io.File - plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' } rootProject.name = 'Iris' -boolean hasVolmLibSettings(File directory) { - new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists() -} - -File resolveLocalVolmLibDirectory() { - String configuredPath = providers.gradleProperty('localVolmLibDirectory') - .orElse(providers.environmentVariable('VOLMLIB_DIR')) - .orNull - if (configuredPath != null && !configuredPath.isBlank()) { - File configuredDirectory = file(configuredPath) - if (hasVolmLibSettings(configuredDirectory)) { - return configuredDirectory - } - } - - File currentDirectory = settingsDir - while (currentDirectory != null) { - File candidate = new File(currentDirectory, 'VolmLib') - if (hasVolmLibSettings(candidate)) { - return candidate - } - - currentDirectory = currentDirectory.parentFile - } - - null -} - -boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib') - .orElse('true') - .map { String value -> value.equalsIgnoreCase('true') } - .get() -File localVolmLibDirectory = resolveLocalVolmLibDirectory() - -if (useLocalVolmLib && localVolmLibDirectory != null) { - includeBuild(localVolmLibDirectory) { - dependencySubstitution { - substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared')) - substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared')) - } - } -} +apply from: new File(settingsDir, 'gradle/volmlib-resolution.settings.gradle') include(':core', ':core:agent') include(':probe') include(':spi') include(':adapters:bukkit:plugin') include(':adapters:bukkit:nms:v26_2_R1') + +/* + * Opt-in: surface the three modded adapter builds to the IDE. + * + * ./gradlew -PincludeModdedAdapters=true ... + * + * OFF by default, and deliberately so. Each adapter settings file does + * `includeBuild('../..')` back onto this build so it can substitute `art.arcane:core` and + * `art.arcane:spi` with the root projects. Including the adapters from here therefore closes a + * composite cycle (root -> adapter -> root) and registers the same VolmLib checkout from two + * participants at once. The release path does not need it: `fabricJar`/`forgeJar`/`neoforgeJar` + * invoke each adapter through its own wrapper as a standalone build (see build.gradle), which is + * also what keeps Loom, ForgeGradle, and ModDevGradle off one plugin classpath. + * + * Enable it only for IDE import, and expect to disable it again if Gradle rejects the cycle. + */ +boolean includeModdedAdapters = providers.gradleProperty('includeModdedAdapters') + .orElse('false') + .map { String value -> value.equalsIgnoreCase('true') } + .get() + +if (includeModdedAdapters) { + includeBuild('adapters/fabric') + includeBuild('adapters/forge') + includeBuild('adapters/neoforge') +} diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java b/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java index 5925e7033..d218603b8 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java @@ -21,6 +21,17 @@ package art.arcane.iris.spi; import java.util.IllegalFormatException; import java.util.regex.Pattern; +/** + * Logging front door for core: routes through the bound {@link IrisPlatform} when there is one and falls back + * to {@code System.out}/{@code System.err} when there is not, so code that runs before adapter startup, in + * tests, or in the standalone probe still logs. + *

+ * Every method is safe from any thread and swallows its own failures - logging never throws. Formatting is + * lenient: a malformed format string is emitted verbatim rather than raising + * {@link java.util.IllegalFormatException}, and a null message renders as {@code "null"}. + *

+ * Internal to Iris; not a published integration surface. + */ public final class IrisLogging { private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]"); private static final Pattern MINI_MESSAGE_TAG = Pattern.compile("(?i)\\n]{0,96})?>|<#[0-9a-f]{6}>"); @@ -28,22 +39,39 @@ public final class IrisLogging { private IrisLogging() { } + /** + * Logs at {@link LogLevel#INFO}, applying {@link #format(String, Object...)} to the arguments. + */ public static void info(String format, Object... args) { emit(LogLevel.INFO, format(format, args)); } + /** + * Logs a literal message at {@link LogLevel#DEBUG}. Takes no format arguments, so a message containing + * {@code %} needs no escaping. + */ public static void debug(String message) { emit(LogLevel.DEBUG, message); } + /** + * Logs at {@link LogLevel#WARN}, applying {@link #format(String, Object...)} to the arguments. + */ public static void warn(String format, Object... args) { emit(LogLevel.WARN, format(format, args)); } + /** + * Logs at {@link LogLevel#ERROR}, applying {@link #format(String, Object...)} to the arguments. + */ public static void error(String format, Object... args) { emit(LogLevel.ERROR, format(format, args)); } + /** + * Emits a player-facing message to the console, preserving colour markup when a platform is bound and + * stripping it via {@link #clean(String)} when one is not. + */ public static void msg(String message) { if (IrisPlatforms.isBound()) { IrisPlatforms.get().msg(message); @@ -53,6 +81,11 @@ public final class IrisLogging { System.out.println("[Iris] " + clean(message)); } + /** + * Logs {@code context} at {@link LogLevel#ERROR} and then reports {@code error}. A null or blank + * {@code context} gets a generic description; a null {@code error} is replaced with a synthetic + * {@link IllegalStateException} so the report is never empty. + */ public static void reportError(String context, Throwable error) { Throwable cause = error == null ? new IllegalStateException("Unknown Iris failure") : error; String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context; @@ -65,9 +98,12 @@ public final class IrisLogging { } reportError(cause); - cause.printStackTrace(System.err); } + /** + * Hands {@code error} to the platform's error reporting, or prints it to {@code System.err} when no + * platform is bound. A null {@code error} is ignored. + */ public static void reportError(Throwable error) { if (IrisPlatforms.isBound()) { IrisPlatforms.get().reportError(error); @@ -80,6 +116,11 @@ public final class IrisLogging { } } + /** + * {@link String#format(String, Object...)} that cannot throw: a null format renders as {@code "null"}, + * no arguments returns {@code format} unchanged, and a mismatched format string is returned verbatim. + * Never returns null. + */ public static String format(String format, Object... args) { if (format == null) { return "null"; @@ -96,6 +137,10 @@ public final class IrisLogging { } } + /** + * Strips legacy section-sign colour codes and MiniMessage tags, for sinks that render neither. A null + * message renders as {@code "null"}. Never returns null. + */ public static String clean(String message) { if (message == null) { return "null"; diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java b/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java index 833799a3e..76f6891a9 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisPlatform.java @@ -22,22 +22,59 @@ import java.io.File; /** * Root platform service provided by each adapter; the single entry point core uses to reach the host platform. + *

+ * Implementations are shared by every Iris thread and must be thread-safe. Accessor methods + * ({@link #registries()}, {@link #scheduler()}, {@link #structureHooks()}, {@link #biomeWriter()}, the + * version and path methods) are called from generation threads and must not block on the server thread. + * The mutating methods ({@link #callEvent(Object)}, {@link #dispatchConsoleCommand(String)}, + * {@link #spawnEntity(PlatformWorld, String, double, double, double)}) touch live server state and are + * expected to be invoked on the thread that owns it - the global/server thread, or the region thread + * owning the target chunk on regionized platforms. Use {@link #scheduler()} to get there. + *

+ * This interface is internal to Iris. It is not a published integration surface and changes without a + * deprecation cycle; adapters in this repository are its only supported implementors. */ public interface IrisPlatform { + /** + * Short adapter identity, for example {@code Bukkit} or the mod loader name. Never null. + */ String platformName(); + /** + * Minecraft version string reported by the host, for example {@code 26.2}. Never null. + */ String minecraftVersion(); + /** + * Registry lookups for blocks, biomes, items and entity types. Never null; may be called off the + * server thread. + */ PlatformRegistries registries(); + /** + * Task dispatch onto the platform's threading model. Never null. + */ PlatformScheduler scheduler(); + /** + * Structure, structure-set and configured-feature access. Never null. + */ PlatformStructureHooks structureHooks(); + /** + * Biome id resolution used when injecting biomes into the mantle. Never null. + */ PlatformBiomeWriter biomeWriter(); + /** + * Root folder Iris owns for packs, settings and generated data. Created if missing. Never null. + */ File dataFolder(); + /** + * {@link #dataFolder()} resolved against {@code path} segments, creating the folder and its parents. + * A null or empty {@code path} returns {@link #dataFolder()}. Never null. + */ default File dataFolder(String... path) { if (path == null || path.length == 0) { return dataFolder(); @@ -48,6 +85,10 @@ public interface IrisPlatform { return folder; } + /** + * Same resolution as {@link #dataFolder(String...)} without creating anything on disk. The returned + * {@link File} may not exist. Never null. + */ default File dataFolderNoCreate(String... path) { if (path == null || path.length == 0) { return dataFolder(); @@ -56,23 +97,72 @@ public interface IrisPlatform { return new File(dataFolder(), String.join(File.separator, path)); } + /** + * A file inside {@link #dataFolder()}, with its parent directories created. The file itself is not + * created. Never null. + */ File dataFile(String... path); + /** + * The Iris artifact this runtime was loaded from: the plugin jar on Bukkit, the mod jar on a mod + * loader. The Bukkit-flavoured name is retained for source compatibility. Never null; adapters that + * cannot locate the real artifact return a placeholder path inside {@link #dataFolder()}. + */ File pluginJar(); + /** + * Iris's own version as a comparable integer, derived from the artifact version. + */ int irisVersionNumber(); + /** + * The host Minecraft version as a comparable integer, derived from {@link #minecraftVersion()}. + */ int minecraftVersionNumber(); + /** + * Publishes an Iris event on the host's event bus. + *

+ * The parameter is untyped by design: the event object is a platform type that this module cannot + * name. On Bukkit it must be an {@code org.bukkit.event.Event} and the adapter casts it; platforms + * without an event bus - every mod loader adapter - ignore the call. Callers therefore must not rely + * on delivery, and must construct events from the platform module that owns the type. {@code event} must + * not be null and must be the type the active adapter expects; a mismatch fails inside the adapter. + *

+ * Invoke on the server thread. Bukkit's event bus is not thread-safe. + */ void callEvent(Object event); + /** + * Runs {@code command} as the server console. Invoke on the server thread. + */ void dispatchConsoleCommand(String command); - boolean spawnEntity(Object world, String entityKey, double x, double y, double z); + /** + * Spawns a vanilla entity by namespaced key at the given block-space position. + *

+ * Adapters unwrap {@link PlatformWorld#nativeHandle()} to reach the host world, so {@code world} must + * be a {@link PlatformWorld} produced by the active adapter. Returns false - never throws - when + * {@code world} or {@code entityKey} is null, the world belongs to a different adapter, the key does + * not parse, the entity type is unknown, or the platform refuses the spawn. + *

+ * Invoke on the thread owning the target chunk. + */ + boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z); + /** + * Routes a log line to the host logger at {@code level}. Safe from any thread. Prefer + * {@link IrisLogging}, which tolerates an unbound platform. + */ void log(LogLevel level, String message); + /** + * Routes a formatted, player-facing message to the console. Safe from any thread. + */ void msg(String message); + /** + * Hands a throwable to the host's error reporting. Safe from any thread; must not rethrow. + */ void reportError(Throwable error); } diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java b/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java index 0c92f5d65..2f8fb56db 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisPlatforms.java @@ -20,6 +20,12 @@ package art.arcane.iris.spi; /** * Static holder binding the active platform adapter for the lifetime of the runtime. + *

+ * Exactly one adapter binds itself during startup; core reaches it through {@link #get()} from every thread. + * The binding is volatile, so reads are safe from any thread and see the bind that happened before them; + * {@link #bind(IrisPlatform)} and {@link #unbind()} serialize against each other. + *

+ * Internal to Iris; not a published integration surface. */ public final class IrisPlatforms { private static volatile IrisPlatform platform; @@ -27,6 +33,11 @@ public final class IrisPlatforms { private IrisPlatforms() { } + /** + * Binds {@code p} as the active platform. Rebinding the same instance is a no-op. + * + * @throws IllegalStateException if a different platform is already bound + */ public static synchronized void bind(IrisPlatform p) { if (platform != null && platform != p) { throw new IllegalStateException("Iris platform is already bound to a different instance"); @@ -34,10 +45,19 @@ public final class IrisPlatforms { platform = p; } + /** + * Clears the binding. Safe to call when nothing is bound. + */ public static synchronized void unbind() { platform = null; } + /** + * The bound platform. Never returns null. + * + * @throws IllegalStateException if no platform is bound, which means Iris was reached before adapter + * startup or after shutdown + */ public static IrisPlatform get() { IrisPlatform bound = platform; if (bound == null) { @@ -46,6 +66,10 @@ public final class IrisPlatforms { return bound; } + /** + * Whether a platform is bound. Use before {@link #get()} on paths that must tolerate a bare JVM, such as + * logging during startup or in tests. + */ public static boolean isBound() { return platform != null; } diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisServices.java b/spi/src/main/java/art/arcane/iris/spi/IrisServices.java index 0da0a3040..bfbbd9e10 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisServices.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisServices.java @@ -20,16 +20,41 @@ package art.arcane.iris.spi; import java.util.concurrent.ConcurrentHashMap; +/** + * Process-wide registry mapping a service interface to the single implementation the active adapter + * installed for it. Core resolves platform-provided collaborators through here instead of importing them. + *

+ * Backed by a {@link ConcurrentHashMap}; every method is safe from any thread. Registration happens during + * adapter startup and removal during shutdown, so a lookup racing a shutdown can legitimately miss - resolve + * lazily at the point of use rather than caching, and prefer {@link #getOrNull(Class)} on optional paths. + *

+ * Internal to Iris; not a published integration surface. + */ public final class IrisServices { private static final ConcurrentHashMap, Object> SERVICES = new ConcurrentHashMap<>(); private IrisServices() { } + /** + * Binds {@code implementation} as the provider for {@code type}, replacing any previous binding. + *

+ * Both parameters are untyped rather than a generic {@code (Class, T)} pair on purpose: callers + * register from wildcard-typed loops - {@code Class} paired with an + * {@code IrisService} instance - and from maps whose value type is the supertype, neither of which a + * generic signature accepts without casts at every call site. The pairing is enforced at runtime by + * {@link Class#cast(Object)}, which throws {@link ClassCastException} on a mismatch, so a wrong pairing + * fails at registration rather than at first use. Null arguments throw. + */ public static void register(Class type, Object implementation) { SERVICES.put(type, type.cast(implementation)); } + /** + * The provider registered for {@code type}. Never returns null. + * + * @throws IllegalStateException if nothing is registered for {@code type} + */ public static T get(Class type) { Object implementation = SERVICES.get(type); if (implementation == null) { @@ -38,15 +63,24 @@ public final class IrisServices { return type.cast(implementation); } + /** + * The provider registered for {@code type}, or null when nothing is registered. + */ public static T getOrNull(Class type) { Object implementation = SERVICES.get(type); return implementation == null ? null : type.cast(implementation); } + /** + * Unbinds {@code type}. No-op when nothing is registered. + */ public static void remove(Class type) { SERVICES.remove(type); } + /** + * Unbinds every service. Called on adapter shutdown and between tests. + */ public static void clear() { SERVICES.clear(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/LogLevel.java b/spi/src/main/java/art/arcane/iris/spi/LogLevel.java index fdd03eaf3..460576b37 100644 --- a/spi/src/main/java/art/arcane/iris/spi/LogLevel.java +++ b/spi/src/main/java/art/arcane/iris/spi/LogLevel.java @@ -19,11 +19,22 @@ package art.arcane.iris.spi; /** - * Severity levels for platform-routed log messages. + * Severity levels for platform-routed log messages. Adapters map these onto the host logger; the unbound + * fallback in {@link IrisLogging} sends {@link #WARN} and {@link #ERROR} to {@code System.err} and the rest to + * {@code System.out}. + *

+ * Constants may be added. Switch expressions over this enum need a {@code default} arm. */ public enum LogLevel { + /** + * Diagnostic detail. Adapters route it to the host logger's own debug channel unless Iris debug logging is + * enabled, so it is normally invisible in server output. + */ DEBUG, + /** Normal operational messages. */ INFO, + /** Recoverable problems and misconfiguration. */ WARN, + /** Failures; usually paired with {@link IrisPlatform#reportError(Throwable)}. */ ERROR } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformBiome.java b/spi/src/main/java/art/arcane/iris/spi/PlatformBiome.java index 64883336d..6f7d44e55 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformBiome.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformBiome.java @@ -20,11 +20,25 @@ package art.arcane.iris.spi; /** * Neutral handle for a resolved biome backed by an adapter-owned native handle. + *

+ * Immutable and safe to share across threads. Internal to Iris; not a published integration surface. + * + * @see PlatformRegistries#biome(String) */ public interface PlatformBiome { + /** + * Canonical {@code namespace:path} biome key. Never null. + */ String key(); + /** + * Namespace half of {@link #key()}. Never null. + */ String namespace(); + /** + * The adapter's backing biome object - {@code org.bukkit.block.Biome} on Bukkit, a {@code Biome} registry + * value on a mod loader. Never null. Only code inside the owning adapter may cast it. + */ Object nativeHandle(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformBiomeWriter.java b/spi/src/main/java/art/arcane/iris/spi/PlatformBiomeWriter.java index bd4903ba4..58f5455fe 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformBiomeWriter.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformBiomeWriter.java @@ -22,9 +22,25 @@ import java.util.List; /** * Resolves pack biome keys to platform biome ids for mantle injection and enumerates the platform's biome registry. + *

+ * Called from generation threads for every biome the pack names, so implementations must be thread-safe and + * should cache their registry lookups. + *

+ * Internal to Iris; not a published integration surface. */ public interface PlatformBiomeWriter { + /** + * The numeric registry id the host uses for {@code key}, which is what gets written into biome storage. + *

+ * Ids are registry-order dependent and therefore valid only for the current server session; never persist + * one. Adapters resolve the key directly, then fall back to a derived match, and finally to a safe default + * id rather than failing - so a nonsense key yields a wrong biome, not an exception. Validate keys with + * {@link PlatformRegistries#biome(String)} when you need to know they exist. + */ int biomeIdFor(String key); + /** + * Every biome in the host registry, including datapack and mod biomes. Never null. + */ List allBiomes(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java b/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java index f8b878a9f..098a8658d 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformBlockProperty.java @@ -20,7 +20,23 @@ package art.arcane.iris.spi; import java.util.List; +/** + * One block state property as JSON schema vocabulary, so pack schema generation can describe block keys without + * knowing platform property types. + *

+ * Immutable. Produced by {@link PlatformRegistries#blockStateProperties()} and consumed only by schema + * generation, never on the generation path. Internal to Iris; not a published integration surface. + * + * @param name the property name as it appears in a block key, for example {@code waterlogged} + * @param jsonType JSON schema type: {@code boolean}, {@code integer} or {@code string}. Never null or empty + * @param defaultValue the value the host's default state carries, boxed as its JSON representation + * @param allowedValues every legal value, empty when the adapter cannot enumerate them. Never null + * @param numericRange bounds for a numeric property, null for {@code boolean} and {@code string} + */ public record PlatformBlockProperty(String name, String jsonType, Object defaultValue, List allowedValues, PlatformNumericRange numericRange) { + /** + * Whether {@link #numericRange()} is present, and therefore whether schema output should emit bounds. + */ public boolean hasNumericRange() { return numericRange != null; } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformBlockState.java b/spi/src/main/java/art/arcane/iris/spi/PlatformBlockState.java index 713a0ad92..23361e451 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformBlockState.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformBlockState.java @@ -20,67 +20,182 @@ package art.arcane.iris.spi; /** * Neutral handle for a resolved block state; the canonical key is the config currency and the native handle is adapter-owned. + *

+ * Instances are immutable value handles, interned by the adapter, and read from generation threads in bulk - + * every predicate here must be a cached field read or cheap computation, never a registry or world lookup. + * Compare with {@link #matches(PlatformBlockState)} rather than {@code equals}; only interned singletons such as + * {@link PlatformRegistries#air()} are safe to compare by identity. + *

+ * Internal to Iris; not a published integration surface. + * + * @see PlatformRegistries#block(String) */ public interface PlatformBlockState { + /** + * Canonical {@code namespace:path[prop=value,...]} key, the form packs and objects store. Never null. + */ String key(); + /** + * Namespace half of {@link #key()}, for example {@code minecraft}. Never null. + */ String namespace(); + /** + * The key with any property block stripped, memoized by implementors that intern their states. + * Returning null means "not memoized"; callers must then derive it from {@link #key()}. + */ + default String materialKey() { + return null; + } + + /** + * Whether this is air, including cave and void air. + */ boolean isAir(); + /** + * Whether the host treats this as a solid collision block. + */ boolean isSolid(); + /** + * Whether this blocks light fully. Drives Iris's own light and surface reasoning. + */ boolean isOccluding(); + /** + * Whether this state came from a custom-content provider rather than a vanilla registry entry. Custom + * states carry a {@link #deferredPlacementKey()} and a {@link #placementBaseState()}. + */ boolean isCustom(); + /** + * For a custom state, the provider key to hand back after the block is written, so the provider can finish + * placement once the chunk exists. Null for ordinary states. + */ default String deferredPlacementKey() { return null; } + /** + * The vanilla state to actually write for a custom state - the placeholder the provider later replaces. + * Returns {@code this} for ordinary states. Never null. + */ default PlatformBlockState placementBaseState() { return this; } + /** + * Whether this is a fluid block, water or lava. + */ boolean isFluid(); + /** + * Whether this is water specifically. + */ boolean isWater(); + /** + * Whether this state carries a {@code waterlogged=true} property. + */ boolean isWaterLogged(); + /** + * Whether this state carries a {@code lit=true} property. + */ boolean isLit(); + /** + * Whether the host needs a block update after this is placed - stairs, fences, redstone and other states + * that resolve their shape from neighbours. + */ boolean isUpdatable(); + /** + * Whether this is plant foliage: grass, ferns, flowers and similar decoration. + */ boolean isFoliage(); + /** + * Whether this is part of a tree - tagged as a log or as leaves. + */ boolean isTreeBlock(); + /** + * Whether foliage can be planted on top of this block. + */ boolean isFoliagePlantable(); + /** + * Whether this is a decorant: a thin block that sits on a surface and cannot support anything. + */ boolean isDecorant(); + /** + * Whether this block has an inventory Iris can fill from a loot table. + */ boolean isStorage(); + /** + * Whether this is specifically a chest, which needs the pairing and orientation handling chests require. + */ boolean isStorageChest(); + /** + * Whether this is an ore block, and therefore a candidate for + * {@link PlatformRegistries#deepSlateOre(PlatformBlockState, PlatformBlockState)}. + */ boolean isOre(); + /** + * Whether this is deepslate or a deepslate variant. + */ boolean isDeepSlate(); + /** + * Whether this is a vine-like block that attaches to a face and hangs. + */ boolean isVineBlock(); + /** + * Whether this block can be placed onto {@code onto} - the support check Iris runs before writing + * decoration. Passing a state from a different adapter fails at runtime. + */ boolean canPlaceOnto(PlatformBlockState onto); + /** + * Whether the two states are the same block with the same properties. Prefer this to {@code equals}, which + * is identity-based on some adapters. + */ boolean matches(PlatformBlockState state); + /** + * Convenience for the common "nothing solid here" test. + */ default boolean isAirOrFluid() { return isAir() || isFluid(); } + /** + * Whether the host attaches a block entity to this block - signs, chests, spawners and similar. Such blocks + * need their tile data applied after the block write. + */ boolean hasTileEntity(); + /** + * This state with one property overridden, merged into {@link #key()} and re-resolved. Never mutates the + * receiver. + *

+ * Strict: a property name or value the block does not accept throws rather than being dropped. Validate + * against {@link PlatformRegistries#blockStateProperties()} first, or resolve the full key through + * {@link PlatformRegistries#blockOrNull(String)} instead. + */ PlatformBlockState withProperty(String name, String value); + /** + * The adapter's backing state object - {@code org.bukkit.block.data.BlockData} on Bukkit, + * {@code BlockState} on a mod loader. Never null. Only code inside the owning adapter may cast it. + */ Object nativeHandle(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformEntityType.java b/spi/src/main/java/art/arcane/iris/spi/PlatformEntityType.java index 8931d0fd4..939f2fcd4 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformEntityType.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformEntityType.java @@ -20,13 +20,32 @@ package art.arcane.iris.spi; /** * Neutral handle for a resolved entity type backed by an adapter-owned native handle. + *

+ * Immutable and safe to share across threads. Internal to Iris; not a published integration surface. + * + * @see PlatformRegistries#entity(String) */ public interface PlatformEntityType { + /** + * Canonical {@code namespace:path} entity type key. Never null. + */ String key(); + /** + * Namespace half of {@link #key()}. Never null. + */ String namespace(); + /** + * The host's spawn category, lowercased - {@code monster}, {@code creature}, {@code ambient} and so on. + * Iris matches it against pack spawn rules, so the string form is the contract rather than any enum. Never + * null. + */ String spawnCategory(); + /** + * The adapter's backing entity type object - {@code org.bukkit.entity.EntityType} on Bukkit, + * {@code EntityType} on a mod loader. Never null. Only code inside the owning adapter may cast it. + */ Object nativeHandle(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformItem.java b/spi/src/main/java/art/arcane/iris/spi/PlatformItem.java index 87a7cc41d..ff86970c6 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformItem.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformItem.java @@ -20,11 +20,26 @@ package art.arcane.iris.spi; /** * Neutral handle for a resolved item type backed by an adapter-owned native handle. + *

+ * Describes an item type, not a stack - no count, no components. Immutable and safe to share across threads. + * Internal to Iris; not a published integration surface. + * + * @see PlatformRegistries#item(String) */ public interface PlatformItem { + /** + * Canonical {@code namespace:path} item key. Never null. + */ String key(); + /** + * Namespace half of {@link #key()}. Never null. + */ String namespace(); + /** + * The adapter's backing item object - {@code org.bukkit.Material} on Bukkit, an {@code Item} registry value + * on a mod loader. Never null. Only code inside the owning adapter may cast it. + */ Object nativeHandle(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java b/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java index b48ec1116..23cedab4e 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformNumericRange.java @@ -18,5 +18,18 @@ package art.arcane.iris.spi; +/** + * Inclusive-or-exclusive numeric bounds for a {@link PlatformBlockProperty}, mirroring JSON schema's + * {@code minimum}/{@code maximum} pair. + *

+ * Immutable. Bounds are carried as {@code double} regardless of the property's JSON type; an + * {@code integer} property's bounds are whole numbers and are narrowed by the schema writer. Internal to Iris; + * not a published integration surface. + * + * @param minimum lower bound + * @param maximum upper bound + * @param exclusiveMinimum whether {@code minimum} itself is disallowed + * @param exclusiveMaximum whether {@code maximum} itself is disallowed + */ public record PlatformNumericRange(double minimum, double maximum, boolean exclusiveMinimum, boolean exclusiveMaximum) { } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java b/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java index d155451eb..9c611cd43 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformRegistries.java @@ -23,39 +23,105 @@ import java.util.Map; /** * Resolves namespaced string keys against the platform's live registries into interned neutral handles. + *

+ * Keys are the pack's currency: {@code namespace:path} with optional {@code [prop=value,...]} block state + * properties. Resolution runs on generation threads for every block a pack names, so implementations must be + * thread-safe and must intern or cache their results - a key that resolves once should not re-parse. + *

+ * Internal to Iris; not a published integration surface. */ public interface PlatformRegistries { + /** + * Resolves a block key through the platform's compatibility layer, which rewrites keys that moved between + * Minecraft versions and consults registered custom-content providers. An unresolvable key is reported and + * falls back to air; a key that cannot be parsed at all may come back null. Callers that need to tell + * absence from air use {@link #blockOrNull(String)}. + */ PlatformBlockState block(String key); + /** + * Resolves a block key, returning null instead of an air fallback when it does not resolve. Silent. + */ PlatformBlockState blockOrNull(String key); + /** + * {@link #blockOrNull(String)} with control over whether an unresolved key is logged. Pass + * {@code warn = false} for speculative lookups. + */ PlatformBlockState blockOrNull(String key, boolean warn); + /** + * The interned air state. Never null; identity-comparable across calls. + */ PlatformBlockState air(); + /** + * The deepslate variant of {@code ore} when {@code block} is deepslate, otherwise {@code ore} unchanged. + * Lets ore placement follow the host stone without the pack enumerating variants. + */ PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore); + /** + * Resolves a biome key against the live biome registry, including datapack and mod biomes. Null when the + * key does not parse or is not registered. + */ PlatformBiome biome(String key); + /** + * Resolves an item key. Null when unknown. + */ PlatformItem item(String key); + /** + * Resolves an entity type key. Null when unknown. + */ PlatformEntityType entity(String key); + /** + * Every registered block state key, properties included. Drives schema completion and command + * suggestions, not the generation path. Never null. + */ List blockKeys(); + /** + * Every registered biome key. Never null. + */ List biomeKeys(); + /** + * Every registered structure key. Never null. + */ List structureKeys(); + /** + * Every registered item key. Never null. + */ List itemKeys(); + /** + * Every registered entity type key. Never null. + */ List entityKeys(); + /** + * Every registered block key without state properties - the material-level view of + * {@link #blockKeys()}. Never null. + */ List blockTypeKeys(); + /** + * Every registered enchantment key. Never null. + */ List enchantmentKeys(); + /** + * Every registered potion effect key. Never null. + */ List potionEffectKeys(); + /** + * Block key to its declared state properties, used to generate pack schema enums and numeric ranges. + * Keyed by material-level block key. Never null. + */ Map> blockStateProperties(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformScheduler.java b/spi/src/main/java/art/arcane/iris/spi/PlatformScheduler.java index 2f560d5f2..2f1c25ffa 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformScheduler.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformScheduler.java @@ -20,15 +20,45 @@ package art.arcane.iris.spi; /** * Platform task dispatch; region scheduling targets the owning region thread on regionized platforms and the global thread elsewhere. + *

+ * Every method is safe to call from any thread. Delivery timing is deliberately unspecified: submitting from the + * thread that already owns the target may run the task inline before returning, or queue it for the next tick, + * depending on the adapter. Callers must assume neither - do not treat return as completion, and do not assume + * the task has not already run. Only a {@code later*} call with a positive delay guarantees a deferral. Tasks that throw are + * reported rather than killing the scheduler, and there is no handle to cancel with; a task that must stop early + * checks its own state. + *

+ * Internal to Iris; not a published integration surface. */ public interface PlatformScheduler { + /** + * Runs {@code task} on the server thread - the global region thread on regionized platforms. + */ void global(Runnable task); + /** + * Runs {@code task} on the thread owning chunk {@code (chunkX, chunkZ)} in {@code world}, which is that + * chunk's region thread on regionized platforms and the server thread elsewhere. Use for anything that + * touches blocks or entities in a known chunk. Adapters that cannot resolve a region owner fall back to + * {@link #global(Runnable)}. + */ void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task); + /** + * Runs {@code task} on a pooled thread off the server thread. Must not touch world state. + */ void async(Runnable task); + /** + * {@link #global(Runnable)} delayed by {@code ticks}. A non-positive delay degrades to a plain + * {@link #global(Runnable)}, inheriting its unspecified timing. + */ void laterGlobal(Runnable task, int ticks); + /** + * {@link #region(PlatformWorld, int, int, Runnable)} delayed by {@code ticks}, with the same non-positive-delay + * degradation as {@link #laterGlobal(Runnable, int)}. Adapters without regions fall back to the delayed global + * queue. + */ void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformStructureHooks.java b/spi/src/main/java/art/arcane/iris/spi/PlatformStructureHooks.java index 3d9b2e4ae..705a81203 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformStructureHooks.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformStructureHooks.java @@ -22,31 +22,81 @@ import java.util.List; /** * Neutral access to the host platform's structure, structure-set and configured-feature registries plus placement entry points. + *

+ * The key enumerations read registries and are safe from any thread. The two placement methods write blocks + * into a live world and must run on the thread owning the target chunks - the region thread on regionized + * platforms, the server thread elsewhere. They are used by the authoring tools that capture vanilla and + * datapack structures into Iris objects, not by the generation path. + *

+ * Internal to Iris; not a published integration surface. */ public interface PlatformStructureHooks { + /** + * Every registered structure key. Never null. + */ List structureKeys(); + /** + * Registered jigsaw structure keys - the subset of {@link #structureKeys()} assembled from template pools. + * Empty when the adapter cannot distinguish them. + */ default List jigsawStructureKeys() { return List.of(); } + /** + * Registered jigsaw template pool keys. Empty when the adapter cannot enumerate them. + */ default List templatePoolKeys() { return List.of(); } + /** + * Every registered structure-set key, the placement grouping that decides structure spacing. Never null. + */ List structureSetKeys(); + /** + * Biome keys the given structure is allowed to generate in. Empty when the structure is unknown or declares + * no biome filter. Never null. + */ List structureBiomeKeys(String structureKey); + /** + * Configured-feature keys that Iris can place as objects - trees, patches and other single-shot features. + * Never null. + */ List objectFeatureKeys(); + /** + * Structure keys actually reachable in {@code world}, after its dimension's structure sets and biome filter + * are applied. Narrower than {@link #structureKeys()}. Never null. + */ List reachableStructureKeys(PlatformWorld world); + /** + * Biome keys the world's biome source can emit. Never null. + */ List possibleBiomeKeys(PlatformWorld world); + /** + * Places a configured feature at world coordinates with the given seed. Returns false when the key is + * unknown or the feature declines to place. Mutates the world; see the threading note on this interface. + */ boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed); + /** + * Generates and places a structure anchored at the given chunk. + * + * @param maxSpan reject the placement if the structure's bounding box exceeds this span in blocks + * @return the placed bounding box as {@code {minX, minY, minZ, maxX, maxY, maxZ}}, or null when the key is + * unknown, the structure produced no valid start, or the box exceeded {@code maxSpan} + */ int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan); + /** + * Whether this adapter implements {@link #placeStructure(PlatformWorld, int, int, String, long, int)}. + * Check before offering structure capture; adapters without the required host access return false. + */ boolean supportsStructurePlacement(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/PlatformWorld.java b/spi/src/main/java/art/arcane/iris/spi/PlatformWorld.java index a80dcdeae..e72f33c44 100644 --- a/spi/src/main/java/art/arcane/iris/spi/PlatformWorld.java +++ b/spi/src/main/java/art/arcane/iris/spi/PlatformWorld.java @@ -20,29 +20,78 @@ package art.arcane.iris.spi; /** * Neutral view of a loaded world for edit and lifecycle paths; never used on the generation hot path. + *

+ * Metadata reads ({@link #name()}, {@link #seed()}, the height bounds) are cheap and safe from any thread. The + * block, biome and weather accessors read or mutate live world state and must be called on the thread that + * owns the target chunk - the region thread on regionized platforms, the server thread elsewhere. Reach that + * thread with {@link PlatformScheduler#region(PlatformWorld, int, int, Runnable)}. + *

+ * Internal to Iris; not a published integration surface. */ public interface PlatformWorld { + /** + * The host's world name. Never null. + */ String name(); + /** + * The world seed, as the host reports it. + */ long seed(); + /** + * Lowest buildable Y, inclusive. Usually negative. + */ int minHeight(); + /** + * Highest buildable Y, exclusive. + */ int maxHeight(); + /** + * The block state at world coordinates. Loads the chunk if it is not resident, so treat it as blocking. + * Adapters delegate straight to the host, so a coordinate outside {@link #minHeight()}/{@link #maxHeight()} + * gets whatever the host does with it - clamp, air, or throw. Bounds-check first. + */ PlatformBlockState getBlock(int x, int y, int z); + /** + * Writes a block state at world coordinates. Same out-of-bounds caveat as {@link #getBlock(int, int, int)}. + * + * @param flags platform update flags; bit 0 requests neighbour/physics updates, higher bits are + * adapter-specific. Pass 0 for a silent write + */ void setBlock(int x, int y, int z, PlatformBlockState block, int flags); + /** + * The biome at world coordinates. Loads the chunk if it is not resident. + */ PlatformBiome getBiome(int x, int y, int z); + /** + * Whether the chunk is currently resident. Cheap; the only accessor here that never triggers a load. + */ boolean isChunkLoaded(int chunkX, int chunkZ); + /** + * The world's time of day in ticks. + */ long getTime(); + /** + * Whether it is raining or snowing. + */ boolean isStorming(); + /** + * Whether a thunderstorm is active. + */ boolean isThundering(); + /** + * The adapter's backing world object - {@code org.bukkit.World} on Bukkit, {@code ServerLevel} on a mod + * loader. Never null. Only code inside the owning adapter may cast it; core must not. + */ Object nativeHandle(); } diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java index 37970bedf..b3daa5566 100644 --- a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessage.java @@ -20,9 +20,24 @@ package art.arcane.iris.spi.protocol; import java.util.List; +/** + * Every message the Iris plugin channel carries, as immutable records under one sealed interface. + *

+ * Sealed so {@link IrisMessageCodec} can switch exhaustively - adding a permitted record forces the codec to + * handle it. Records are immutable and safe to hand between threads, except that {@link VisionTile} holds its + * payload array by reference and must not be mutated after construction. + *

+ * Internal to Iris; not a published integration surface. + */ public sealed interface IrisMessage { + /** + * The {@code IrisProtocol.TYPE_*} discriminator written as the first field of the encoded frame. + */ int messageTypeId(); + /** + * Client to server, first frame: the client's protocol version and the capability bits it wants. + */ record ClientHello(int protocolVersion, long capabilities) implements IrisMessage { @Override public int messageTypeId() { @@ -30,6 +45,10 @@ public sealed interface IrisMessage { } } + /** + * Server to client, handshake reply: the version and capability bits actually granted, the server brand, and + * whether any Iris world is live. + */ record ServerHello(int protocolVersion, long capabilities, String serverBrand, boolean irisActive) implements IrisMessage { @Override public int messageTypeId() { @@ -37,8 +56,13 @@ public sealed interface IrisMessage { } } + /** + * Server to client: periodic pregeneration progress for one job. + */ record PregenProgress(long jobId, long chunksDone, long chunksTotal, double chunksPerSecond, long etaMillis, int state) implements IrisMessage { + /** The job is generating. */ public static final int STATE_RUNNING = 0; + /** The job is paused and will resume. */ public static final int STATE_PAUSED = 1; @Override @@ -47,6 +71,10 @@ public sealed interface IrisMessage { } } + /** + * Server to client: a pregeneration job stopped. {@code completed} distinguishes finishing from being + * cancelled or failing. + */ record PregenEnd(long jobId, boolean completed) implements IrisMessage { @Override public int messageTypeId() { @@ -54,6 +82,10 @@ public sealed interface IrisMessage { } } + /** + * Server to client: which pack and height bounds back the dimension the player is in. {@code irisWorld} is + * false for a vanilla dimension, in which case the other fields are placeholders. + */ record DimensionStatus(String dimensionKey, String packKey, long seed, int minY, int maxY, boolean irisWorld) implements IrisMessage { @Override public int messageTypeId() { @@ -61,6 +93,10 @@ public sealed interface IrisMessage { } } + /** + * Client to server: what does the generator say about this column. Rate-limited by + * {@link IrisProtocol#MAX_INBOUND_FRAMES_PER_SECOND}. + */ record CursorInfoRequest(int blockX, int blockZ) implements IrisMessage { @Override public int messageTypeId() { @@ -68,6 +104,10 @@ public sealed interface IrisMessage { } } + /** + * Server to client: the answer to a {@link CursorInfoRequest}. {@code caveBiomeKey} is empty when no cave + * biome applies at that column. + */ record CursorInfo(int blockX, int blockZ, String biomeKey, String regionKey, String caveBiomeKey, int height, String dimensionKey) implements IrisMessage { @Override public int messageTypeId() { @@ -75,6 +115,10 @@ public sealed interface IrisMessage { } } + /** + * Client to server: render and send one vision map tile. Rate-limited by + * {@link IrisProtocol#MAX_VISION_TILE_REQUESTS_PER_SECOND}. + */ record VisionTileRequest(int tileX, int tileZ, int zoomLevel) implements IrisMessage { @Override public int messageTypeId() { @@ -82,6 +126,12 @@ public sealed interface IrisMessage { } } + /** + * Server to client: one chunk of a rendered tile. A tile larger than + * {@link IrisProtocol#VISION_TILE_MAX_CHUNK_BYTES} arrives as {@code chunkCount} frames sharing a + * {@code sequence}; the client reassembles by {@code chunkIndex} and discards a partial set when the + * sequence changes. {@code data} is held by reference - do not mutate it after construction. + */ record VisionTile(int tileX, int tileZ, int zoomLevel, int sequence, int chunkIndex, int chunkCount, byte[] data) implements IrisMessage { @Override public int messageTypeId() { @@ -89,7 +139,14 @@ public sealed interface IrisMessage { } } + /** + * Server to client: point-of-interest overlay for a tile, capped at {@link IrisProtocol#MAX_VISION_MARKERS} + * entries. + */ record VisionMarkers(int tileX, int tileZ, int zoomLevel, List markers) implements IrisMessage { + /** + * One overlay marker at a block position. {@code kind} is a client-side icon selector. + */ public record Marker(int blockX, int blockZ, int kind, String label) { } @@ -99,9 +156,16 @@ public sealed interface IrisMessage { } } + /** + * Server to client: one region of a pregeneration job changed state. Sent as a delta so the client can paint + * a live progress grid without a full snapshot per tick. + */ record PregenRegionDelta(long jobId, int regionX, int regionZ, int state) implements IrisMessage { + /** Queued, not started. */ public static final int STATE_PENDING = 0; + /** Currently generating. */ public static final int STATE_GENERATING = 1; + /** Finished and saved. */ public static final int STATE_DONE = 2; @Override @@ -110,6 +174,10 @@ public sealed interface IrisMessage { } } + /** + * Server to client: a studio pack reload happened. {@code failed} marks a reload that did not apply, with + * {@code message} carrying the reason. + */ record StudioHotload(String packKey, int changedFiles, boolean failed, String message) implements IrisMessage { @Override public int messageTypeId() { @@ -117,10 +185,17 @@ public sealed interface IrisMessage { } } + /** + * Server to client: show a transient notification. + */ record Toast(int kind, String title, String body) implements IrisMessage { + /** Neutral notice. */ public static final int KIND_INFO = 0; + /** Operation succeeded. */ public static final int KIND_SUCCESS = 1; + /** Something needs attention. */ public static final int KIND_WARNING = 2; + /** Operation failed. */ public static final int KIND_ERROR = 3; @Override diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java index b4f507c77..c8f5d0938 100644 --- a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisMessageCodec.java @@ -21,10 +21,25 @@ package art.arcane.iris.spi.protocol; import java.util.ArrayList; import java.util.List; +/** + * Encodes and decodes {@link IrisMessage} frames for the Iris plugin channel. + *

+ * Stateless; safe from any thread. Each call allocates its own {@link IrisWireWriter} or + * {@link IrisWireReader}, so no instance is shared. Encoding and decoding must stay symmetric - the field order + * in each switch arm is the wire format. + *

+ * Internal to Iris; not a published integration surface. + */ public final class IrisMessageCodec { private IrisMessageCodec() { } + /** + * Encodes {@code message} into a frame: type id as a varint, then the record's fields in declaration order. + * Never returns null. + * + * @throws IllegalStateException if the encoded form exceeds {@link IrisProtocol#MAX_FRAME_BYTES} + */ public static byte[] encode(IrisMessage message) { IrisWireWriter writer = new IrisWireWriter(); writer.writeVarInt(message.messageTypeId()); @@ -120,6 +135,14 @@ public final class IrisMessageCodec { return writer.toByteArray(); } + /** + * Decodes a frame produced by {@link #encode(IrisMessage)}. + * + * @return the decoded message, or null when the type id is unknown - which is how a newer peer's messages are + * skipped rather than treated as corruption. Callers must handle null. + * @throws ProtocolException if {@code frame} is null, exceeds {@link IrisProtocol#MAX_FRAME_BYTES}, or is + * truncated or otherwise malformed + */ public static IrisMessage decode(byte[] frame) throws ProtocolException { if (frame == null) { throw new ProtocolException("null frame"); diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java index 80e3966d5..5d53beda7 100644 --- a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisProtocol.java @@ -18,18 +18,40 @@ package art.arcane.iris.spi.protocol; +/** + * Wire constants for the Iris client/server plugin channel: version, channel name, size and rate caps, + * capability bits and message type ids. + *

+ * Both ends of the channel compile against this class, so every value here is part of the wire contract. Adding + * a message type or capability bit is compatible; changing an existing value is not and requires bumping + * {@link #PROTOCOL_VERSION}. Constants only - no state, safe from any thread. + *

+ * Internal to Iris; not a published integration surface. + */ public final class IrisProtocol { + /** Wire version exchanged in the hello handshake. Bumped on any incompatible change. */ public static final int PROTOCOL_VERSION = 1; + /** Plugin channel both ends register. */ public static final String CHANNEL = "irisworldgen:main"; + /** Hard cap on a single encoded frame. {@link IrisWireWriter} refuses to exceed it; the decoder rejects larger. */ public static final int MAX_FRAME_BYTES = 24576; + /** Inbound frames accepted per client per second before the server sheds. */ public static final int MAX_INBOUND_FRAMES_PER_SECOND = 32; + /** Vision tile requests accepted per client per second, tighter than the general frame budget because each one costs a render. */ public static final int MAX_VISION_TILE_REQUESTS_PER_SECOND = 8; + /** Fixed header size of a vision tile frame, subtracted when splitting a tile into chunks. */ public static final int VISION_TILE_HEADER_BYTES = 25; + /** Largest payload carried by one vision tile chunk. */ public static final int VISION_TILE_MAX_CHUNK_BYTES = 24000; + /** Cap on markers in one {@link IrisMessage.VisionMarkers} frame. */ public static final int MAX_VISION_MARKERS = 256; + /** Capability bit: pregeneration progress streaming. */ public static final long CAPABILITY_PREGEN = 1L << 0; + /** Capability bit: vision map tiles and markers. */ public static final long CAPABILITY_VISION = 1L << 1; + /** Capability bit: cursor coordinate lookups. */ public static final long CAPABILITY_CURSOR = 1L << 2; + /** Capability bit: studio hotload notifications. */ public static final long CAPABILITY_STUDIO = 1L << 3; public static final int TYPE_CLIENT_HELLO = 1; public static final int TYPE_SERVER_HELLO = 2; diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java index e14a6ec49..e0d30de4a 100644 --- a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireReader.java @@ -20,17 +20,37 @@ package art.arcane.iris.spi.protocol; import java.nio.charset.StandardCharsets; +/** + * Sequential big-endian reader over one protocol frame. + *

+ * Not thread-safe and not reusable: it carries a read position, so one instance serves one frame on one thread. + * Every read is bounds-checked against the frame length and throws {@link ProtocolException} rather than + * {@link ArrayIndexOutOfBoundsException}, so a hostile or truncated frame cannot read past its end. Length + * prefixes are validated against the remaining bytes before any allocation, so a forged length cannot force a + * large allocation. + *

+ * Internal to Iris; not a published integration surface. + */ public final class IrisWireReader { private final byte[] frame; private final int limit; private int position; + /** + * Wraps {@code frame} for reading from offset zero. The array is held by reference and must not be mutated + * while the reader is in use. + */ public IrisWireReader(byte[] frame) { this.frame = frame; this.limit = frame.length; this.position = 0; } + /** + * Reads a 7-bit-continuation varint. + * + * @throws ProtocolException if the frame ends mid-varint or the encoding exceeds five bytes + */ public int readVarInt() throws ProtocolException { int result = 0; int shift = 0; @@ -46,6 +66,11 @@ public final class IrisWireReader { throw new ProtocolException("varint exceeds 5 bytes"); } + /** + * Reads a fixed four-byte big-endian int. + * + * @throws ProtocolException if fewer than four bytes remain + */ public int readInt() throws ProtocolException { requireRemaining(4); int value = ((frame[position] & 0xFF) << 24) @@ -56,6 +81,11 @@ public final class IrisWireReader { return value; } + /** + * Reads a fixed eight-byte big-endian long. + * + * @throws ProtocolException if fewer than eight bytes remain + */ public long readLong() throws ProtocolException { requireRemaining(8); long value = ((long) (frame[position] & 0xFF) << 56) @@ -70,15 +100,30 @@ public final class IrisWireReader { return value; } + /** + * Reads a double from its IEEE 754 bit pattern. + * + * @throws ProtocolException if fewer than eight bytes remain + */ public double readDouble() throws ProtocolException { return Double.longBitsToDouble(readLong()); } + /** + * Reads one byte as a boolean; any non-zero value is true. + * + * @throws ProtocolException if no bytes remain + */ public boolean readBoolean() throws ProtocolException { requireRemaining(1); return frame[position++] != 0; } + /** + * Reads a varint-length-prefixed UTF-8 string. Never returns null; an empty string is legal. + * + * @throws ProtocolException if the length is negative or exceeds the remaining bytes + */ public String readString() throws ProtocolException { int declaredLength = readVarInt(); if (declaredLength < 0) { @@ -92,6 +137,11 @@ public final class IrisWireReader { return value; } + /** + * Reads a varint-length-prefixed byte array into a fresh copy. Never returns null. + * + * @throws ProtocolException if the length is negative or exceeds the remaining bytes + */ public byte[] readBytes() throws ProtocolException { int declaredLength = readVarInt(); if (declaredLength < 0) { diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java index a56263b2a..c462c6477 100644 --- a/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/IrisWireWriter.java @@ -21,15 +21,31 @@ package art.arcane.iris.spi.protocol; import java.nio.charset.StandardCharsets; import java.util.Arrays; +/** + * Sequential big-endian writer that builds one protocol frame into a growing byte buffer. + *

+ * Not thread-safe: it carries a write position, so one instance serves one frame on one thread. The buffer + * doubles as needed and is hard-capped at {@link IrisProtocol#MAX_FRAME_BYTES} - exceeding it throws + * {@link IllegalStateException} at the write that overflows rather than emitting an oversized frame the peer + * would reject. Field order here is the wire format and must mirror {@link IrisWireReader}. + *

+ * Internal to Iris; not a published integration surface. + */ public final class IrisWireWriter { private byte[] buffer; private int length; + /** + * Creates an empty writer with a small buffer that grows on demand. + */ public IrisWireWriter() { this.buffer = new byte[64]; this.length = 0; } + /** + * Writes a 7-bit-continuation varint. Negative values encode as five bytes. + */ public void writeVarInt(int value) { int remaining = value; while (true) { @@ -44,6 +60,9 @@ public final class IrisWireWriter { } } + /** + * Writes a fixed four-byte big-endian int. + */ public void writeInt(int value) { ensure(4); buffer[length++] = (byte) (value >>> 24); @@ -52,6 +71,9 @@ public final class IrisWireWriter { buffer[length++] = (byte) value; } + /** + * Writes a fixed eight-byte big-endian long. + */ public void writeLong(long value) { ensure(8); buffer[length++] = (byte) (value >>> 56); @@ -64,14 +86,23 @@ public final class IrisWireWriter { buffer[length++] = (byte) value; } + /** + * Writes a double as its IEEE 754 bit pattern. + */ public void writeDouble(double value) { writeLong(Double.doubleToLongBits(value)); } + /** + * Writes a boolean as one byte, {@code 1} or {@code 0}. + */ public void writeBoolean(boolean value) { writeByte(value ? 1 : 0); } + /** + * Writes a varint-length-prefixed UTF-8 string. {@code value} must not be null. + */ public void writeString(String value) { byte[] encoded = value.getBytes(StandardCharsets.UTF_8); writeVarInt(encoded.length); @@ -80,6 +111,9 @@ public final class IrisWireWriter { length += encoded.length; } + /** + * Writes a varint-length-prefixed byte array, copying the contents. {@code value} must not be null. + */ public void writeBytes(byte[] value) { writeVarInt(value.length); ensure(value.length); @@ -87,6 +121,10 @@ public final class IrisWireWriter { length += value.length; } + /** + * The bytes written so far, as a fresh copy trimmed to length. The writer stays usable afterwards. Never + * returns null. + */ public byte[] toByteArray() { return Arrays.copyOf(buffer, length); } diff --git a/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java b/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java index 84c6857d6..ee419834f 100644 --- a/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java +++ b/spi/src/main/java/art/arcane/iris/spi/protocol/ProtocolException.java @@ -18,7 +18,19 @@ package art.arcane.iris.spi.protocol; +/** + * Signals a malformed protocol frame: truncated, over the size cap, or carrying an impossible length prefix. + *

+ * Checked on purpose - a bad frame is an expected condition on a public channel, not a bug, and the handler is + * meant to drop the frame and carry on rather than propagate. Never used for an unknown message type; + * {@link IrisMessageCodec#decode(byte[])} returns null for that. + *

+ * Internal to Iris; not a published integration surface. + */ public class ProtocolException extends Exception { + /** + * @param message what was wrong with the frame, including the byte counts involved where relevant + */ public ProtocolException(String message) { super(message); }