From 03746468ec93109284bc6ed8534a94d90fb5dd46 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 16 Aug 2026 23:53:42 -0400 Subject: [PATCH] COntent --- ...ucturePostProcessorSurfaceTerrainTest.java | 50 +++ .../src/main/java/art/arcane/iris/Iris.java | 6 +- .../iris/core/commands/CommandIris.java | 22 +- .../iris/core/commands/CommandStudio.java | 43 +- ...ommandIrisCreateOverwriteContractTest.java | 24 ++ .../NativeStructureSurfaceFitter.java | 101 ++++- .../NativeStructureTemplateOccupancy.java | 18 + .../iris/modded/ModdedBlockResolution.java | 6 +- .../ModdedBlockResolutionContractTest.java | 11 + core/purity-allowlist.txt | 2 + .../iris/core/IrisDatapackCompiler.java | 118 ++++++ .../arcane/iris/core/ServerConfigurator.java | 50 ++- .../BukkitCommandMessagesExtended.java | 10 + .../localization/BukkitRuntimeMessages.java | 12 +- .../localization/RuntimeProgressMessages.java | 48 ++- .../core/pack/PackBiomeLayerValidator.java | 65 ++- .../arcane/iris/core/pack/PackValidator.java | 2 + .../project/StudioOpenProgressReporter.java | 86 +--- .../iris/core/runtime/ChunkJobReporter.java | 73 +--- .../core/runtime/StudioOpenCoordinator.java | 37 +- .../service/PackDownloadProgressReporter.java | 103 ++--- .../arcane/iris/core/service/StudioSVC.java | 16 +- .../arcane/iris/core/tools/IrisConverter.java | 43 +- .../arcane/iris/core/tools/IrisCreator.java | 221 +++++----- .../tools/WorldCreationProgressReporter.java | 399 ++++++++++++++++++ .../iris/engine/decorator/DecoratorCore.java | 44 +- .../decorator/IrisShoreLineDecorator.java | 10 +- .../framework/EngineAssignedWorldManager.java | 11 +- .../NativeStructureOwnershipStore.java | 31 +- .../components/MantleObjectComponent.java | 12 +- .../engine/modifier/IrisCarveModifier.java | 288 +++++++------ .../engine/modifier/IrisCarveScratch.java | 17 +- .../engine/modifier/IrisDepositModifier.java | 131 +++++- .../arcane/iris/engine/object/IrisBiome.java | 4 +- .../iris/engine/object/IrisDecorator.java | 2 +- .../engine/object/IrisDepositBiomeScope.java | 6 + .../engine/object/IrisDepositGenerator.java | 237 ++++++++++- .../object/IrisDepositHeightDistribution.java | 7 + .../object/IrisDepositPlacementScope.java | 7 + .../iris/engine/object/IrisDepositShape.java | 7 + .../iris/engine/object/IrisFormation.java | 47 ++- .../iris/engine/object/IrisFormationForm.java | 12 +- .../object/IrisObjectPlacementRunner.java | 8 +- .../object/formation/FormationGenerator.java | 4 + .../formation/FormationShapeBuilder.java | 284 +++++++++++-- .../iris/platform/bukkit/BukkitPlatform.java | 20 +- .../iris/util/common/plugin/VolmitSender.java | 78 +--- .../iris/util/common/scheduling/jobs/Job.java | 36 +- core/src/main/resources/languages/de_DE.json | 30 +- core/src/main/resources/languages/es_ES.json | 30 +- core/src/main/resources/languages/fi_FI.json | 30 +- core/src/main/resources/languages/fr_FR.json | 30 +- core/src/main/resources/languages/he_IL.json | 30 +- core/src/main/resources/languages/it_IT.json | 30 +- core/src/main/resources/languages/ja-JP.json | 30 +- core/src/main/resources/languages/ko_KR.json | 30 +- core/src/main/resources/languages/lt_LT.json | 30 +- core/src/main/resources/languages/nl_NL.json | 30 +- core/src/main/resources/languages/pl_PL.json | 30 +- core/src/main/resources/languages/pt_PT.json | 30 +- core/src/main/resources/languages/ru_RU.json | 30 +- core/src/main/resources/languages/tr_TR.json | 30 +- core/src/main/resources/languages/vi_VI.json | 30 +- core/src/main/resources/languages/zh_CN.json | 30 +- core/src/main/resources/languages/zh_TW.json | 30 +- .../iris/core/IrisDatapackCompilerTest.java | 64 +++ ...erConfiguratorDatapackFingerprintTest.java | 21 + .../pack/PackBiomeLayerValidatorTest.java | 38 +- .../StudioOpenCoordinatorOpenKindTest.java | 25 ++ .../PackDownloadProgressReporterTest.java | 12 +- ...tudioSVCWorldPackFeedbackContractTest.java | 33 ++ .../IrisCreatorProgressContractTest.java | 46 ++ .../WorldCreationProgressReporterTest.java | 85 ++++ .../engine/decorator/DecoratorCoreTest.java | 45 ++ .../decorator/IrisShoreLineDecoratorTest.java | 1 + .../NativeStructureOwnershipStoreTest.java | 68 ++- ...leObjectComponentProceduralChanceTest.java | 18 + .../IrisCarveModifierBoundarySupportTest.java | 104 +++++ ...isCarveModifierInferenceIsolationTest.java | 6 +- .../engine/modifier/IrisCarveScratchTest.java | 5 +- .../IrisDepositModifierContainmentTest.java | 102 +++++ .../IrisBiomeCeilingLayerGuardTest.java | 5 + .../object/IrisDecoratorHeightTest.java | 30 ++ .../engine/object/IrisDepositTuningTest.java | 64 +++ .../object/NativeStructureObjectVetoTest.java | 11 + .../formation/FormationShapeBuilderTest.java | 352 +++++++++++++++ 86 files changed, 3681 insertions(+), 833 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java create mode 100644 core/src/test/java/art/arcane/iris/core/service/StudioSVCWorldPackFeedbackContractTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/tools/IrisCreatorProgressContractTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/tools/WorldCreationProgressReporterTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentProceduralChanceTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisDecoratorHeightTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/formation/FormationShapeBuilderTest.java 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 2826eab70..be7a7f319 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 @@ -262,6 +262,43 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 11, 70, 1)); } + @Test + public void terrainMatchingFootprintsRaiseAndLowerOnlyLowestSolidColumns() throws Exception { + StructureTemplate pathTemplate = template(List.of( + block(0, 0, 0, Blocks.AIR.defaultBlockState()), + block(0, 1, 0, Blocks.DIRT_PATH.defaultBlockState()), + block(15, 0, 0, Blocks.AIR.defaultBlockState()), + block(30, 0, 0, Blocks.AIR.defaultBlockState()), + block(30, 1, 0, Blocks.DIRT_PATH.defaultBlockState()), + block(45, 0, 0, Blocks.DANDELION.defaultBlockState()))); + InlineSinglePoolElement element = new InlineSinglePoolElement( + pathTemplate, List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING); + PoolElementStructurePiece piece = rigidTemplatePiece( + element, new BoundingBox(0, 68, 0, 45, 74, 0), 1, Rotation.NONE); + StructureStart start = rigidSurfaceStart( + List.of(piece), TerrainAdjustment.BEARD_THIN); + BoundingBox area = new BoundingBox(0, 52, 0, 45, 76, 0); + Map blocks = flatTerrain(area, 64); + blocks.remove(new BlockPos(0, 63, 0)); + blocks.remove(new BlockPos(0, 64, 0)); + put(blocks, 0, 59, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 60, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 30, 71, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 30, 72, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + + NativeStructureSurfaceFitter.prepareSurfaceStructures( + world(blocks), area, List.of(surfaceTarget(start)), + (x, z) -> x == 0 ? 60 : x == 30 ? 72 : 64); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 68, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 68, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 30, 72, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 65, 0)); + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 45, 64, 0)); + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 45, 65, 0)); + } + @Test public void vacuumFitsOnlyProcessedSolidTemplateColumns() throws Exception { StructureTemplate sparseTemplate = template(List.of( @@ -2171,6 +2208,19 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { if (methodName.equals("getSeed")) { return TEST_SEED; } + if (methodName.equals("getHeight")) { + int x = (int) arguments[1]; + int z = (int) arguments[2]; + int highest = Integer.MIN_VALUE; + for (Map.Entry entry : blocks.entrySet()) { + BlockPos position = entry.getKey(); + if (position.getX() == x && position.getZ() == z + && !entry.getValue().isAir()) { + highest = Math.max(highest, position.getY()); + } + } + return highest == Integer.MIN_VALUE ? 0 : highest + 1; + } if (methodName.equals("getLevel")) { return null; } 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 3d66ff799..8f8ae6bad 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 @@ -93,8 +93,8 @@ import art.arcane.volmlib.util.exceptions.IrisException; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.function.NastyRunnable; import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; +import art.arcane.volmlib.util.hud.HudActionBar; import art.arcane.volmlib.util.hud.HudBossBarLane; -import art.arcane.volmlib.util.hud.HudSlotService; import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.io.InstanceState; import art.arcane.volmlib.util.math.M; @@ -549,7 +549,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { SimdSupport.install(); services = new KMap<>(); setupAudience(); - BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane()); + BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane()); Bindings.setupSentry(); // Explicit, ordered service list: the previous reflective jar scan gave hash-ordered // enable/disable and paid a full-jar class sweep at boot. Infrastructure first, @@ -777,7 +777,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { removeShutdownHook(); } if (BukkitPlatform.hasHud()) { - BukkitPlatform.hudSlots().shutdown(); + BukkitPlatform.hudBar().shutdown(); BukkitPlatform.hudLanes().shutdown(); } if (configHotloadEngine != null) { 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 d3107721c..cb5ad5391 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 @@ -148,10 +148,15 @@ public class CommandIris implements DirectorExecutor { IrisDimension dimension = IrisToolbelt.getDimension(resolvedType); if (dimension == null) { - sender().sendMessage("Could not find dimension '" + resolvedType + "'."); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND, + MessageArgument.untrusted("dimension", resolvedType) + )); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND)); - sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType) - + " and restart the server."); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART, + MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType)) + )); return; } @@ -227,10 +232,15 @@ public class CommandIris implements DirectorExecutor { : type; IrisDimension dimension = IrisToolbelt.getDimension(resolvedType); if (dimension == null) { - sender().sendMessage("Could not find dimension '" + resolvedType + "'."); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND, + MessageArgument.untrusted("dimension", resolvedType) + )); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND)); - sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType) - + " and restart the server."); + sender().sendMessage(IrisLanguage.text( + BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART, + MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType)) + )); return; } 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 560bc9131..ca41119b0 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 @@ -77,18 +77,12 @@ import art.arcane.iris.util.common.plugin.VolmitSender; 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.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 org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.FluidCollisionMode; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.World; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; @@ -108,7 +102,6 @@ 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; import art.arcane.iris.core.localization.IrisLanguage; @@ -343,8 +336,7 @@ public class CommandStudio implements DirectorExecutor { // previously leaked the whole sampler ForkJoinPool, the self-rescheduling // progress task and both HUD claims for the rest of the server's uptime. MultiBurst multiBurst = null; - HudSlotClaim titleClaim = null; - HudSlotClaim barClaim = null; + boolean progressShown = false; int c = -1; try { engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0))); @@ -354,31 +346,11 @@ public class CommandStudio implements DirectorExecutor { var loc = player.getLocation(); int totalTasks = d * d; AtomicInteger completedTasks = new AtomicInteger(0); - titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE))); - barClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR))); - HudSlotClaim finalTitleClaim = titleClaim; - HudSlotClaim finalBarClaim = barClaim; - AtomicLong lastResolveMs = new AtomicLong(0L); + progressShown = true; c = J.ar(() -> { - long now = System.currentTimeMillis(); - if (now - lastResolveMs.get() >= 250L) { - lastResolveMs.set(now); - finalTitleClaim.resolve(); - finalBarClaim.resolve(); - } double jobProgress = (double) completedTasks.get() / totalTasks; - HudSurface barSurface = finalBarClaim.granted(); - sender.sendProgress( - jobProgress, - IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS), - finalTitleClaim.granted(), - barSurface - ); - if (barSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, BarColor.BLUE, BarStyle.SOLID, 4000L); - } else if (barSurface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(player, "iris:job"); - } + sender.sendProgress(jobProgress, IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS)); + BukkitPlatform.showProgressLane(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, 4000L); }, 0); new Spiraler(d, d, (x, z) -> executor.queue(() -> { var region = engine.getRegion((x << 4) + 8, (z << 4) + 8); @@ -399,11 +371,8 @@ public class CommandStudio implements DirectorExecutor { if (c != -1) { J.car(c); } - if (titleClaim != null) { - titleClaim.release(); - } - if (barClaim != null) { - barClaim.release(); + if (progressShown) { + sender.sendAction(" "); BukkitPlatform.hudLanes().hide(player, "iris:job"); } if (multiBurst != null) { diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java index 1ad9b1891..da86065d6 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandIrisCreateOverwriteContractTest.java @@ -7,6 +7,8 @@ import org.junit.Test; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import static org.junit.Assert.assertEquals; @@ -71,4 +73,26 @@ public class CommandIrisCreateOverwriteContractTest { assertEquals(Long.valueOf(Long.MAX_VALUE), handler.parse(Long.toString(Long.MAX_VALUE), false)); assertThrows(DirectorParsingException.class, () -> handler.parse("9223372036854775808", false)); } + + @Test + public void createAndReplaceUseLocalizedStyledMissingDimensionFeedback() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/commands/CommandIris.java" + )); + + assertEquals(2, occurrences(source, "COMMAND_IRIS_DIMENSION_NOT_FOUND")); + assertEquals(2, occurrences(source, "COMMAND_IRIS_INSTALL_PACK_AND_RESTART")); + assertFalse(source.contains("sendMessage(\"Could not find dimension")); + assertFalse(source.contains("sendMessage(\"Install its pack with")); + } + + private static int occurrences(String value, String match) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(match, offset)) >= 0) { + count++; + offset += match.length(); + } + return count; + } } 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 index 57db72007..a71134827 100644 --- 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 @@ -33,7 +33,9 @@ public final class NativeStructureSurfaceFitter { private static final double SURFACE_TERRAIN_FALLOFF = 2.0; private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L; private static final int SURFACE_TERRAIN_RADIUS = 12; - private static final int MAX_VACUUM_TEMPLATE_CELLS = 4_194_304; + private static final int MAX_SURFACE_TEMPLATE_CELLS = 4_194_304; + private static final Set WARNED_SOURCE_BUDGET = + ConcurrentHashMap.newKeySet(); private static final Set WARNED_VACUUM_BUDGET = ConcurrentHashMap.newKeySet(); @@ -45,23 +47,24 @@ public final class NativeStructureSurfaceFitter { List targets, IntBinaryOperator surfaceHeight) { return prepareSurfaceStructures( - world, area, targets, surfaceHeight, MAX_VACUUM_TEMPLATE_CELLS); + world, area, targets, surfaceHeight, MAX_SURFACE_TEMPLATE_CELLS); } static VacuumFoundationPlan prepareSurfaceStructures( WorldGenLevel world, BoundingBox area, List targets, - IntBinaryOperator surfaceHeight, int maximumVacuumTemplateCells) { + IntBinaryOperator surfaceHeight, int maximumTemplateCells) { if (targets == null || targets.isEmpty()) { return VacuumFoundationPlan.empty(); } Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver"); - List anchors = collectSourceSurfaceAnchors(targets); + List anchors = collectSourceSurfaceAnchors( + world, area, targets, maximumTemplateCells); if (!anchors.isEmpty()) { fitSurfaceTerrain(world, area, anchors, surfaceHeight); } VacuumFootprint vacuum = collectVacuumFootprint( - world, area, targets, maximumVacuumTemplateCells); + world, area, targets, maximumTemplateCells); if (!vacuum.anchors().isEmpty()) { fitVacuumTerrain(world, area, vacuum, surfaceHeight); } @@ -223,7 +226,14 @@ public final class NativeStructureSurfaceFitter { } private static List collectSourceSurfaceAnchors( - List targets) { + WorldGenLevel world, BoundingBox area, + List targets, + int maximumTemplateCells) { + BoundingBox influenceArea = new BoundingBox( + area.minX() - SURFACE_TERRAIN_RADIUS, area.minY(), + area.minZ() - SURFACE_TERRAIN_RADIUS, + area.maxX() + SURFACE_TERRAIN_RADIUS, area.maxY(), + area.maxZ() + SURFACE_TERRAIN_RADIUS); List anchors = new ArrayList<>(); for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { if (!requiresSourceSurfaceTerrain(target)) { @@ -231,13 +241,34 @@ public final class NativeStructureSurfaceFitter { } StructureStart start = target.start(); TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); + BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox(); + BlockPos firstCenter = firstBounds.getCenter(); + BlockPos referencePosition = new BlockPos( + firstCenter.getX(), firstBounds.minY(), firstCenter.getZ()); + TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells); + boolean budgetExceeded = false; for (StructurePiece piece : start.getPieces()) { if (piece instanceof PoolElementStructurePiece poolPiece) { - if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) { + StructureTemplatePool.Projection projection = + poolPiece.getElement().getProjection(); + if (projection == StructureTemplatePool.Projection.RIGID) { BoundingBox bounds = poolPiece.getBoundingBox(); anchors.add(surfaceAnchor( bounds, bounds.minY() + poolPiece.getGroundLevelDelta(), 2, adjustment)); + } else if (!budgetExceeded + && projection == StructureTemplatePool.Projection.TERRAIN_MATCHING + && intersectsHorizontally(poolPiece.getBoundingBox(), influenceArea)) { + try { + List pieceAnchors = new ArrayList<>(); + addTerrainMatchingSurfaceAnchors( + world, influenceArea, poolPiece, referencePosition, + adjustment, budget, pieceAnchors); + anchors.addAll(pieceAnchors); + } catch (TemplateBudgetExceeded ignored) { + budgetExceeded = true; + warnSourceBudget(target); + } } for (JigsawJunction junction : poolPiece.getJunctions()) { anchors.add(new SurfaceAnchor( @@ -255,6 +286,42 @@ public final class NativeStructureSurfaceFitter { return List.copyOf(anchors); } + private static void addTerrainMatchingSurfaceAnchors( + WorldGenLevel world, BoundingBox influenceArea, + PoolElementStructurePiece piece, BlockPos referencePosition, + TerrainAdjustment adjustment, TemplateCellBudget budget, + List anchors) { + NativeStructureTemplateOccupancy.OccupancyResult occupancy = + NativeStructureTemplateOccupancy.resolve( + world, piece, referencePosition, influenceArea, + () -> world.getLevel().getStructureManager(), + influenceArea::isInside, budget::consume); + if (!occupancy.resolved()) { + return; + } + int groundY = piece.getBoundingBox().minY() + piece.getGroundLevelDelta(); + Map lowest = + NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells()); + for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) { + budget.consume(1); + BoundingBox column = new BoundingBox( + cell.x(), groundY, cell.z(), cell.x(), groundY, cell.z()); + anchors.add(surfaceAnchor(column, groundY, 2, adjustment)); + } + } + + private static void warnSourceBudget( + NativeStructureTerrainIntegrator.TerrainTarget target) { + String structureId = target.structureId() == null + ? target.start().getStructure().getClass().getName() + : target.structureId(); + if (WARNED_SOURCE_BUDGET.add(structureId)) { + IrisLogging.warn("Native structure SOURCE fitting for '" + + structureId + "' exceeded its bounded template budget; " + + "skipping remaining terrain-matching footprints for this start"); + } + } + private static VacuumFootprint collectVacuumFootprint( WorldGenLevel world, BoundingBox area, List targets, @@ -286,7 +353,7 @@ public final class NativeStructureSurfaceFitter { Map targetCaps = new HashMap<>(); Set targetFoundationBases = new HashSet<>(); Set targetOccupiedCells = new HashSet<>(); - VacuumCellBudget budget = new VacuumCellBudget(maximumTemplateCells); + TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells); try { for (StructurePiece piece : start.getPieces()) { BoundingBox bounds = piece.getBoundingBox(); @@ -330,7 +397,7 @@ public final class NativeStructureSurfaceFitter { cell.x(), cell.y(), cell.z())); } } - } catch (VacuumBudgetExceeded ignored) { + } catch (TemplateBudgetExceeded ignored) { warnVacuumBudget(target); continue; } @@ -837,31 +904,31 @@ public final class NativeStructureSurfaceFitter { } } - private static final class VacuumCellBudget { + private static final class TemplateCellBudget { private final int maximum; private int consumed; - private VacuumCellBudget(int maximum) { + private TemplateCellBudget(int maximum) { if (maximum < 0) { throw new IllegalArgumentException( - "Native structure vacuum cell budget cannot be negative"); + "Native structure surface template budget cannot be negative"); } this.maximum = maximum; } private void consume(int amount) { if (amount < 0 || consumed > maximum - amount) { - throw VacuumBudgetExceeded.INSTANCE; + throw TemplateBudgetExceeded.INSTANCE; } consumed += amount; } } - private static final class VacuumBudgetExceeded extends RuntimeException { - private static final VacuumBudgetExceeded INSTANCE = - new VacuumBudgetExceeded(); + private static final class TemplateBudgetExceeded extends RuntimeException { + private static final TemplateBudgetExceeded INSTANCE = + new TemplateBudgetExceeded(); - private VacuumBudgetExceeded() { + private TemplateBudgetExceeded() { super(null, null, false, false); } } diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java index ba2682ca4..f904a2127 100644 --- a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureTemplateOccupancy.java @@ -89,6 +89,24 @@ final class NativeStructureTemplateOccupancy { return lowest; } + static Map lowestProcessedSolidCells(Map occupancy) { + Map lowest = new HashMap<>(); + for (Map.Entry entry : occupancy.entrySet()) { + OccupancyCell cell = entry.getValue(); + if (cell.blocker() || !isSolidBase(cell.state())) { + continue; + } + BlockPos position = BlockPos.of(entry.getKey()); + long column = columnKey(position.getX(), position.getZ()); + LowestCell current = lowest.get(column); + if (current == null || position.getY() < current.y()) { + lowest.put(column, new LowestCell( + position.getX(), position.getY(), position.getZ(), cell)); + } + } + return lowest; + } + static boolean isSolidBase(BlockState state) { return state != null && state.isSolid() && !state.is(Blocks.STRUCTURE_VOID) 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 b8c96597c..77d73be35 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 @@ -113,7 +113,7 @@ public final class ModdedBlockResolution { "poppy", "dandelion", "oxeye_daisy", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip", "lilac", "dead_bush", "sweet_berry_bush", "rose_bush", "wither_rose", "allium", "blue_orchid", "lily_of_the_valley", "crimson_fungus", "warped_fungus", "red_mushroom", "brown_mushroom", - "crimson_roots", "azure_bluet", "weeping_vines", "weeping_vines_plant", "warped_roots", + "crimson_roots", "azure_bluet", "cactus", "weeping_vines", "weeping_vines_plant", "warped_roots", "nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes", "carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass", "acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button", @@ -510,6 +510,10 @@ public final class ModdedBlockResolution { } public static boolean canPlaceOnto(Block mat, Block onto) { + if (mat == Blocks.CACTUS) { + return onto == Blocks.CACTUS || onto == Blocks.SAND || onto == Blocks.RED_SAND; + } + if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM) && (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS || mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java index d5138586c..ce3613743 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java @@ -7,8 +7,10 @@ import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * The SPI splits block resolution into a null-returning lookup and an air-falling-back lookup. Modded used to @@ -49,4 +51,13 @@ public class ModdedBlockResolutionContractTest { assertNotNull(state); assertEquals(Blocks.OAK_LOG, state.handle().getBlock()); } + + @Test + public void cactusPlacementRequiresNativeCactusSupport() { + assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.CACTUS)); + assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.SAND)); + assertTrue(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.RED_SAND)); + assertFalse(ModdedBlockResolution.canPlaceOnto(Blocks.CACTUS, Blocks.STONE)); + assertTrue(ModdedBlockResolution.isDecorant(Blocks.CACTUS.defaultBlockState())); + } } diff --git a/core/purity-allowlist.txt b/core/purity-allowlist.txt index edae3b97f..b49452462 100644 --- a/core/purity-allowlist.txt +++ b/core/purity-allowlist.txt @@ -68,6 +68,7 @@ art/arcane/iris/core/service/JigsawStudioService.java art/arcane/iris/core/service/JigsawStudioToolCodec.java art/arcane/iris/core/service/ObjectSVC.java art/arcane/iris/core/service/ObjectStudioSaveService.java +art/arcane/iris/core/service/PackDownloadProgressReporter.java art/arcane/iris/core/service/StudioSVC.java art/arcane/iris/core/service/TreeSVC.java art/arcane/iris/core/structure/BulkStructureImporter.java @@ -81,6 +82,7 @@ art/arcane/iris/core/tools/IrisPackBenchmarking.java art/arcane/iris/core/tools/IrisReflectiveAPI.java art/arcane/iris/core/tools/IrisToolbelt.java art/arcane/iris/core/tools/IrisWorldCreator.java +art/arcane/iris/core/tools/WorldCreationProgressReporter.java art/arcane/iris/engine/IrisEngineEffects.java art/arcane/iris/engine/IrisWorldManager.java art/arcane/iris/engine/MarkerSpawnScanner.java diff --git a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java index 30c5e2363..feade6483 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java +++ b/core/src/main/java/art/arcane/iris/core/IrisDatapackCompiler.java @@ -8,6 +8,8 @@ import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.datapack.IDataFixer; import art.arcane.iris.core.pack.AtomicDirectoryPublisher; import art.arcane.iris.core.pack.PackDirectoryResolver; +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.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; @@ -31,17 +33,22 @@ import java.util.Collection; import java.util.Comparator; import java.util.HexFormat; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.regex.Pattern; import java.util.stream.Stream; public final class IrisDatapackCompiler { private static final int INPUT_FINGERPRINT_SCHEMA = 2; private static final int INPUT_BUFFER_BYTES = 64 * 1024; private static final int WORLD_PACK_SCAN_DEPTH = 8; + private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); private static final List INPUT_DIRECTORIES = List.of("dimensions", "biomes", "snippet"); private static final String FLAT_VOID_LEVEL_STEM = """ { @@ -164,6 +171,117 @@ public final class IrisDatapackCompiler { return HexFormat.of().formatHex(digest.digest()); } + public static Map computeRegistryRequirements( + List packRoots, + IDataFixer fixer + ) throws IOException { + Objects.requireNonNull(packRoots, "packRoots"); + Objects.requireNonNull(fixer, "fixer"); + LinkedHashMap requirements = new LinkedHashMap<>(); + Set installedBiomeKeys = new LinkedHashSet<>(); + for (File packRoot : packRoots) { + PackDirectoryResolver.requireSafePackTree(packRoot); + if (!hasDimensions(packRoot.toPath())) { + continue; + } + IrisData data = IrisData.openDatapackCompiler(packRoot); + try { + ResourceLoader loader = data.getDimensionLoader(); + String[] possibleKeys = loader.getPossibleKeys(); + if (possibleKeys == null || possibleKeys.length == 0) { + throw new IOException("Iris pack has no dimension definitions: " + packRoot); + } + for (String possibleKey : possibleKeys) { + IrisDimension dimension = loader.load(possibleKey); + if (dimension == null) { + throw new IOException("Unable to load Iris dimension '" + possibleKey + "' from " + packRoot); + } + collectRegistryRequirements( + requirements, + installedBiomeKeys, + dimension, + fixer); + } + } finally { + data.close(); + } + } + return Map.copyOf(requirements); + } + + public static Map computeRegistryRequirements( + IrisDimension dimension, + IDataFixer fixer + ) throws IOException { + IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension"); + IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer"); + LinkedHashMap requirements = new LinkedHashMap<>(); + collectRegistryRequirements( + requirements, + new LinkedHashSet<>(), + requiredDimension, + requiredFixer); + return Map.copyOf(requirements); + } + + private static void collectRegistryRequirements( + Map requirements, + Set installedBiomeKeys, + IrisDimension dimension, + IDataFixer fixer + ) { + requirements.put( + "dimension_type/iris:" + dimension.getDimensionTypeKey(), + fingerprintContent(dimension.getDimensionType().toJson(fixer))); + + String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT); + for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) { + if (biome == null || !biome.isCustom()) { + continue; + } + String derivativeKey = biome.getVanillaDerivativeKey(); + for (IrisBiomeCustom customBiome : biome.getCustomDerivitives()) { + if (customBiome == null) { + continue; + } + String biomeKey = namespace + ":" + customBiome.getId(); + if (!installedBiomeKeys.add(biomeKey)) { + continue; + } + requirements.put( + "worldgen/biome/" + biomeKey, + fingerprintContent(customBiome.generateJson(fixer))); + TreeSet tags = new TreeSet<>(); + for (String tag : customBiome.getEffectiveTags(derivativeKey)) { + String normalizedTag = normalizeRegistryKey(tag); + if (normalizedTag != null) { + tags.add(normalizedTag); + } + } + requirements.put( + "worldgen/biome_tags/" + biomeKey, + fingerprintContent(String.join("\n", tags))); + } + } + } + + private static String normalizeRegistryKey(String key) { + if (key == null || key.isBlank()) { + return null; + } + String normalized = key.trim().toLowerCase(Locale.ROOT); + if (normalized.indexOf(':') < 0) { + normalized = "minecraft:" + normalized; + } + return REGISTRY_KEY_PATTERN.matcher(normalized).matches() ? normalized : null; + } + + private static String fingerprintContent(String content) { + MessageDigest digest = sha256(); + updateDigestString(digest, Objects.requireNonNull(content, "registry content")); + return HexFormat.of().formatHex(digest.digest()); + } + public static String compilerIdentity(IDataFixer fixer) { IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer"); return String.join( diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index ba88ca5cd..05de7503b 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -91,6 +91,7 @@ public class ServerConfigurator { private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024; private static volatile boolean loadedDatapackRuntimeReady; private static volatile String loadedDatapackCompilerInputFingerprint = ""; + private static volatile Map loadedDatapackRegistryRequirements = Map.of(); private static volatile long loadedDatapackRuntimeGeneration; private static volatile boolean loadedDatapackRestartRequired; @@ -98,6 +99,7 @@ public class ServerConfigurator { synchronized (DATAPACK_INSTALL_LOCK) { invalidateLoadedDatapackRuntime(); loadedDatapackCompilerInputFingerprint = ""; + loadedDatapackRegistryRequirements = Map.of(); loadedDatapackRestartRequired = false; } IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration(); @@ -112,13 +114,15 @@ public class ServerConfigurator { if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) { loadedDatapackRuntimeReady = !IrisSettings.get().getGeneral().adjustVanillaHeight && pinLoadedDatapackCompilerInputs( - DefaultPackBootstrapProvisioner.compilerInputFingerprintThisStartup()); + DefaultPackBootstrapProvisioner.compilerInputFingerprintThisStartup()) + && pinLoadedDatapackRegistryRequirements(); IrisLogging.info("Paper loaded the Iris datapack during bootstrap; skipping the legacy startup install."); } else { DatapackInstallResult result = installDataPacks(true); loadedDatapackRuntimeReady = result.succeeded() && !result.restartRequired() - && pinLoadedDatapackCompilerInputs(); + && pinLoadedDatapackCompilerInputs() + && pinLoadedDatapackRegistryRequirements(); if (result.restartRequired()) { IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them."); } @@ -138,12 +142,13 @@ public class ServerConfigurator { || INMS.get().missingDimensionTypes(requiredDimension.getDimensionTypeKey())) { return false; } - String currentFingerprint = computeCurrentDatapackCompilerInputFingerprint(resolveDataFixer()); - return reusableRuntimeFingerprint( - loadedDatapackCompilerInputFingerprint, - currentFingerprint); + Map requiredRegistryEntries = + IrisDatapackCompiler.computeRegistryRequirements(requiredDimension, resolveDataFixer()); + return loadedRegistrySatisfies( + loadedDatapackRegistryRequirements, + requiredRegistryEntries); } catch (IOException | RuntimeException exception) { - IrisLogging.reportError("Unable to verify loaded Iris datapack compiler inputs.", exception); + IrisLogging.reportError("Unable to verify loaded Iris datapack registry requirements.", exception); return false; } } @@ -514,6 +519,37 @@ public class ServerConfigurator { } } + private static boolean pinLoadedDatapackRegistryRequirements() { + if (loadedDatapackRestartRequired) { + return false; + } + try { + loadedDatapackRegistryRequirements = IrisDatapackCompiler.computeRegistryRequirements( + collectCompilerPackRoots(), + resolveDataFixer()); + return true; + } catch (IOException | RuntimeException exception) { + loadedDatapackRegistryRequirements = Map.of(); + IrisLogging.reportError("Unable to pin loaded Iris datapack registry requirements.", exception); + return false; + } + } + + static boolean loadedRegistrySatisfies( + Map loadedRequirements, + Map requiredEntries + ) { + if (loadedRequirements == null || requiredEntries == null || requiredEntries.isEmpty()) { + return false; + } + for (Map.Entry entry : requiredEntries.entrySet()) { + if (!entry.getValue().equals(loadedRequirements.get(entry.getKey()))) { + return false; + } + } + return true; + } + static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) { return loadedFingerprint != null && !loadedFingerprint.isBlank() diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java index 4c659c982..356d1ee3d 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitCommandMessagesExtended.java @@ -175,6 +175,14 @@ public final class BukkitCommandMessagesExtended { "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend", C.YELLOW + "Try one of: overworld, vanilla, flat, theend" ); + public static final TextKey COMMAND_IRIS_DIMENSION_NOT_FOUND = TextKey.of( + "iris.bukkit.commandiris.dimension_not_found", + C.RED + "Could not find dimension " + C.WHITE + "{dimension}" + C.RED + "." + ); + public static final TextKey COMMAND_IRIS_INSTALL_PACK_AND_RESTART = TextKey.of( + "iris.bukkit.commandiris.install_pack_and_restart", + C.YELLOW + "Install it with " + C.AQUA + "{command}" + C.YELLOW + " and restart the server." + ); public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD = TextKey.of( "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load", C.GREEN + "World staging completed. Iris is restarting the server to generate/load \"" + "{worldName}" + "\"." @@ -864,6 +872,8 @@ public final class BukkitCommandMessagesExtended { COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2, COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS, COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND, + COMMAND_IRIS_DIMENSION_NOT_FOUND, + COMMAND_IRIS_INSTALL_PACK_AND_RESTART, COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD, COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS, COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD, diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java index 0197675f6..bb5cbdace 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java @@ -408,7 +408,15 @@ public final class BukkitRuntimeMessages { ); public static final TextKey STUDIO_S_V_C_INSTALLING_PACKAGE = TextKey.of( "iris.bukkit.runtime.studiosvc.installing_package", - "Installing Package: " + "{name}" + ":" + "{loadKey}" + C.GOLD + "World pack " + C.AQUA + "{name}" + ":" + "{loadKey}" + C.GRAY + " | " + C.WHITE + "Publishing snapshot" + ); + public static final TextKey STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD = TextKey.of( + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread", + C.RED + "Iris refused to copy the world pack on the Bukkit primary thread." + ); + public static final TextKey STUDIO_S_V_C_PACK_INSTALL_FAILED = TextKey.of( + "iris.bukkit.runtime.studiosvc.pack_install_failed", + C.RED + "Failed to install world pack " + C.WHITE + "{dimension}" + C.RED + ": {error}" ); public static final TextKey STUDIO_S_V_C_LOOKING_PACKAGE = TextKey.of( "iris.bukkit.runtime.studiosvc.looking_package", @@ -701,6 +709,8 @@ public final class BukkitRuntimeMessages { IRIS_CONVERTER_CONVERTED_3, IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS, STUDIO_S_V_C_INSTALLING_PACKAGE, + STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD, + STUDIO_S_V_C_PACK_INSTALL_FAILED, STUDIO_S_V_C_LOOKING_PACKAGE, STUDIO_S_V_C_FOUND_IRIS_FOLDER, STUDIO_S_V_C_FOUND_DIMENSION_FOLDER_REPACKAGING, diff --git a/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java b/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java index f846049d7..c88443071 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/RuntimeProgressMessages.java @@ -31,8 +31,28 @@ public final class RuntimeProgressMessages { public static final TextKey STUDIO_STAGE_FINALIZE_OPEN = TextKey.of("iris.runtime.studio.stage.finalize_open", "Finalizing"); public static final TextKey STUDIO_STAGE_CLEANUP = TextKey.of("iris.runtime.studio.stage.cleanup", "Cleaning up"); public static final TextKey WORLD_CREATE_TELEPORT_FAILED = TextKey.of("iris.runtime.world_create.teleport_failed", C.YELLOW + "The world was created, but automatic teleport failed. Try /iris teleport world={world}"); - public static final TextKey WORLD_CREATE_ACTION = TextKey.of("iris.runtime.world_create.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.DARK_GRAY + " {generated}/{required} chunks"); - public static final TextKey WORLD_CREATE_CONSOLE = TextKey.of("iris.runtime.world_create.console", C.GOLD + "Generating " + C.YELLOW + "{percent}%" + C.GRAY + " {generated}/{required} chunks" + C.DARK_GRAY + " ({remaining} left)"); + public static final TextKey WORLD_CREATE_BOSSBAR_WORKING = TextKey.of("iris.runtime.world_create.bossbar.working", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.WHITE + "Starting"); + public static final TextKey WORLD_CREATE_BOSSBAR_PROGRESS = TextKey.of("iris.runtime.world_create.bossbar.progress", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.YELLOW + "{percent}% " + C.WHITE + "{stage}"); + public static final TextKey WORLD_CREATE_BOSSBAR_FAILED = TextKey.of("iris.runtime.world_create.bossbar.failed", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.RED + "FAILED " + C.DARK_GRAY + "{percent}%"); + public static final TextKey WORLD_CREATE_BOSSBAR_READY = TextKey.of("iris.runtime.world_create.bossbar.ready", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.GREEN + "READY 100%"); + public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION = TextKey.of("iris.runtime.world_create.lifecycle.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "{stage}{detail}" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION_FAILED = TextKey.of("iris.runtime.world_create.lifecycle.action.failed", "{bar}" + C.GRAY + " " + C.RED + "FAILED" + C.GRAY + " | " + C.WHITE + "{stage}{detail}" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_ACTION_READY = TextKey.of("iris.runtime.world_create.lifecycle.action.ready", "{bar}" + C.GRAY + " " + C.GREEN + "100%" + C.GRAY + " | " + C.GREEN + "World ready" + C.DARK_GRAY + " {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_CONSOLE = TextKey.of("iris.runtime.world_create.lifecycle.console", C.GOLD + "World " + C.AQUA + "{world} {bar} " + C.YELLOW + "{percent}%" + C.GRAY + " {stage}{detail}" + C.DARK_GRAY + " ({elapsed})"); + public static final TextKey WORLD_CREATE_LIFECYCLE_CONSOLE_FAILED = TextKey.of("iris.runtime.world_create.lifecycle.console.failed", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.RED + "creation failed" + C.DARK_GRAY + " after {elapsed}"); + public static final TextKey WORLD_CREATE_LIFECYCLE_CONSOLE_READY = TextKey.of("iris.runtime.world_create.lifecycle.console.ready", C.GOLD + "World " + C.AQUA + "{world}" + C.GRAY + " | " + C.GREEN + "ready" + C.DARK_GRAY + " in {elapsed}"); + public static final TextKey WORLD_CREATE_STAGE_INITIALIZING = TextKey.of("iris.runtime.world_create.stage.initializing", "Initializing"); + public static final TextKey WORLD_CREATE_STAGE_RESOLVE_DIMENSION = TextKey.of("iris.runtime.world_create.stage.resolve_dimension", "Resolving dimension"); + public static final TextKey WORLD_CREATE_STAGE_VALIDATE_PACK = TextKey.of("iris.runtime.world_create.stage.validate_pack", "Validating pack"); + public static final TextKey WORLD_CREATE_STAGE_PREPARE_WORLD_PACK = TextKey.of("iris.runtime.world_create.stage.prepare_world_pack", "Preparing world pack"); + public static final TextKey WORLD_CREATE_STAGE_INSTALL_DATAPACKS = TextKey.of("iris.runtime.world_create.stage.install_datapacks", "Installing datapacks"); + public static final TextKey WORLD_CREATE_STAGE_PREPARE_GENERATOR = TextKey.of("iris.runtime.world_create.stage.prepare_generator", "Preparing generator"); + public static final TextKey WORLD_CREATE_STAGE_CREATE_WORLD = TextKey.of("iris.runtime.world_create.stage.create_world", "Generating spawn"); + public static final TextKey WORLD_CREATE_STAGE_REGISTER_WORLD = TextKey.of("iris.runtime.world_create.stage.register_world", "Registering world"); + public static final TextKey WORLD_CREATE_STAGE_TELEPORT_PLAYER = TextKey.of("iris.runtime.world_create.stage.teleport_player", "Finding safe entry"); + public static final TextKey WORLD_CREATE_STAGE_PREGENERATE = TextKey.of("iris.runtime.world_create.stage.pregenerate", "Pregenerating"); + public static final TextKey WORLD_CREATE_STAGE_FINALIZE = TextKey.of("iris.runtime.world_create.stage.finalize", "Finalizing"); + public static final TextKey WORLD_CREATE_STAGE_COMPLETE = TextKey.of("iris.runtime.world_create.stage.complete", "World ready"); public static final TextKey WORLD_PREGEN_ACTION = TextKey.of("iris.runtime.world_create.pregen.action", "{bar}" + C.GRAY + " " + C.YELLOW + "{percent}%" + C.GRAY + " | " + C.WHITE + "Pregenerating"); public static final TextKey WORLD_PREGEN_CONSOLE = TextKey.of("iris.runtime.world_create.pregen.console", C.GOLD + "Pregenerating " + C.YELLOW + "{percent}%"); public static final TextKey CHUNK_TITLE_REGEN = TextKey.of("iris.runtime.chunk_job.title.regen", "Regen"); @@ -110,8 +130,28 @@ public final class RuntimeProgressMessages { STUDIO_STAGE_FINALIZE_OPEN, STUDIO_STAGE_CLEANUP, WORLD_CREATE_TELEPORT_FAILED, - WORLD_CREATE_ACTION, - WORLD_CREATE_CONSOLE, + WORLD_CREATE_BOSSBAR_WORKING, + WORLD_CREATE_BOSSBAR_PROGRESS, + WORLD_CREATE_BOSSBAR_FAILED, + WORLD_CREATE_BOSSBAR_READY, + WORLD_CREATE_LIFECYCLE_ACTION, + WORLD_CREATE_LIFECYCLE_ACTION_FAILED, + WORLD_CREATE_LIFECYCLE_ACTION_READY, + WORLD_CREATE_LIFECYCLE_CONSOLE, + WORLD_CREATE_LIFECYCLE_CONSOLE_FAILED, + WORLD_CREATE_LIFECYCLE_CONSOLE_READY, + WORLD_CREATE_STAGE_INITIALIZING, + WORLD_CREATE_STAGE_RESOLVE_DIMENSION, + WORLD_CREATE_STAGE_VALIDATE_PACK, + WORLD_CREATE_STAGE_PREPARE_WORLD_PACK, + WORLD_CREATE_STAGE_INSTALL_DATAPACKS, + WORLD_CREATE_STAGE_PREPARE_GENERATOR, + WORLD_CREATE_STAGE_CREATE_WORLD, + WORLD_CREATE_STAGE_REGISTER_WORLD, + WORLD_CREATE_STAGE_TELEPORT_PLAYER, + WORLD_CREATE_STAGE_PREGENERATE, + WORLD_CREATE_STAGE_FINALIZE, + WORLD_CREATE_STAGE_COMPLETE, WORLD_PREGEN_ACTION, WORLD_PREGEN_CONSOLE, CHUNK_TITLE_REGEN, diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java index a13e249cc..17297aa22 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackBiomeLayerValidator.java @@ -34,7 +34,6 @@ import java.util.List; * engine skips them, and this validator surfaces the mistake to the author at validate time. */ final class PackBiomeLayerValidator { - /** Both layers and caveCeilingLayers default to a single entry when absent (IrisBiome field initializers). */ private static final int DEFAULT_LAYER_COUNT = 1; private PackBiomeLayerValidator() { @@ -58,8 +57,8 @@ final class PackBiomeLayerValidator { continue; } - Integer layers = arrayLength(biome, "layers", biomeKey, blockingErrors); - Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, blockingErrors); + Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors); + Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors); if (layers == null || ceiling == null) { continue; } @@ -72,9 +71,65 @@ final class PackBiomeLayerValidator { return blockingErrors; } - private static Integer arrayLength(JSONObject biome, String field, String biomeKey, List blockingErrors) { + static List validateDecoratorPalettes(File biomesFolder, File decoratorSnippetsFolder) { + List blockingErrors = new ArrayList<>(); + if (biomesFolder != null && biomesFolder.isDirectory()) { + List biomeFiles = PackValidationIo.listJsonRecursive(biomesFolder); + biomeFiles.sort(Comparator.comparing(File::getPath)); + for (File biomeFile : biomeFiles) { + String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile); + JSONObject biome = PackValidationIo.readJson(biomeFile); + if (biome == null || !biome.has("decorators") || biome.isNull("decorators")) { + continue; + } + JSONArray decorators = biome.optJSONArray("decorators"); + if (decorators == null) { + blockingErrors.add("Biome '" + biomeKey + "' decorators must be an array."); + continue; + } + validateDecoratorArray(decorators, "Biome '" + biomeKey + "' decorators", blockingErrors); + } + } + + if (decoratorSnippetsFolder != null && decoratorSnippetsFolder.isDirectory()) { + List snippetFiles = PackValidationIo.listJsonRecursive(decoratorSnippetsFolder); + snippetFiles.sort(Comparator.comparing(File::getPath)); + for (File snippetFile : snippetFiles) { + String snippetKey = PackValidationIo.deriveKey(decoratorSnippetsFolder, snippetFile); + JSONObject snippet = PackValidationIo.readJson(snippetFile); + if (snippet != null) { + validateDecoratorPalette(snippet, "Decorator snippet '" + snippetKey + "'", blockingErrors); + } + } + } + return blockingErrors; + } + + private static void validateDecoratorArray(JSONArray decorators, String path, List blockingErrors) { + for (int index = 0; index < decorators.length(); index++) { + Object rawDecorator = decorators.opt(index); + if (rawDecorator instanceof String) { + continue; + } + if (!(rawDecorator instanceof JSONObject decorator)) { + blockingErrors.add(path + "[" + index + "] must be an object or snippet reference."); + continue; + } + validateDecoratorPalette(decorator, path + "[" + index + "]", blockingErrors); + } + } + + private static void validateDecoratorPalette(JSONObject decorator, String path, List blockingErrors) { + JSONArray palette = decorator.optJSONArray("palette"); + if (palette == null || palette.length() == 0) { + blockingErrors.add(path + " must declare a non-empty palette."); + } + } + + private static Integer arrayLength(JSONObject biome, String field, String biomeKey, int defaultCount, + List blockingErrors) { if (!biome.has(field) || biome.isNull(field)) { - return DEFAULT_LAYER_COUNT; + return defaultCount; } JSONArray array = biome.optJSONArray(field); 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 b89c0d6a2..a66070d44 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 @@ -99,6 +99,8 @@ public final class PackValidator { blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns( new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory)); blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes"))); + blockingErrors.addAll(PackBiomeLayerValidator.validateDecoratorPalettes( + new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator"))); PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder); addDistinct(blockingErrors, styledRanges.errors()); addDistinct(warnings, styledRanges.warnings()); 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 index f54448336..0c872bb96 100644 --- a/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java +++ b/core/src/main/java/art/arcane/iris/core/project/StudioOpenProgressReporter.java @@ -21,21 +21,13 @@ 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; @@ -53,7 +45,6 @@ final class StudioOpenProgressReporter { 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( @@ -64,15 +55,8 @@ final class StudioOpenProgressReporter { 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(() -> { @@ -95,26 +79,14 @@ final class StudioOpenProgressReporter { 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); - } + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_FAILED, + MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), + MessageArgument.trusted("stage", currentStage) + )); } else { sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2)); } @@ -126,26 +98,14 @@ final class StudioOpenProgressReporter { 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); - } + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.STUDIO_ACTION_READY, + MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); } else { sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1))))); } @@ -162,25 +122,13 @@ final class StudioOpenProgressReporter { )); } - 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); - } + 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 { long now = System.currentTimeMillis(); long nextUpdate = nextConsoleUpdate.get(); diff --git a/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java b/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java index e91ae5e64..f98eecd2f 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/ChunkJobReporter.java @@ -20,17 +20,12 @@ package art.arcane.iris.core.runtime; 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.spi.IrisLogging; import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.math.ChunkSpiral; 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.World; @@ -62,7 +57,6 @@ public final class ChunkJobReporter { private final AtomicInteger failures = new AtomicInteger(0); private volatile int total = 0; private volatile long startMs = 0L; - private volatile HudSlotClaim claim; public ChunkJobReporter(VolmitSender sender, String title, World world) { this.sender = sender; @@ -127,15 +121,6 @@ public final class ChunkJobReporter { bossBar.addPlayer(sender.player()); bossBar.setVisible(true); } - if (player) { - claim = BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest( - "iris:chunk-job", - HudPriority.PROGRESS, - 1200L, - List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR) - )); - } - AtomicInteger taskId = new AtomicInteger(-1); taskId.set(J.ar(() -> { double currentProgress = Math.max(0.0D, Math.min(1.0D, progress.get())); @@ -157,28 +142,15 @@ public final class ChunkJobReporter { MessageArgument.trusted("percent", percent) )); } - if (sender.isPlayer() && claim != null) { - HudSurface surface = claim.resolve(); - if (surface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:chunk-job"); - sender.sendAction(IrisLanguage.text( - RuntimeProgressMessages.CHUNK_ACTION_PROGRESS, - MessageArgument.trusted("bar", progressBar(currentProgress)), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("stage", stage.get()), - MessageArgument.trusted("applied", applied.get()), - MessageArgument.trusted("total", total <= 0 ? "?" : total) - )); - } else if (surface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:chunk-job", IrisLanguage.text( - RuntimeProgressMessages.CHUNK_ACTION_PROGRESS, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("stage", stage.get()), - MessageArgument.trusted("applied", applied.get()), - MessageArgument.trusted("total", total <= 0 ? "?" : total) - ), currentProgress, BarColor.GREEN, BarStyle.SOLID, 4000L); - } + if (sender.isPlayer()) { + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.CHUNK_ACTION_PROGRESS, + MessageArgument.trusted("bar", progressBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", stage.get()), + MessageArgument.trusted("applied", applied.get()), + MessageArgument.trusted("total", total <= 0 ? "?" : total) + )); } }, REPORT_INTERVAL_TICKS)); if (complete.get()) { @@ -214,30 +186,15 @@ public final class ChunkJobReporter { J.a(() -> { bossBar.removeAll(); bossBar.setVisible(false); - HudSlotClaim finishedClaim = claim; - if (finishedClaim != null) { - finishedClaim.release(); - BukkitPlatform.hudLanes().hide(sender.player(), "iris:chunk-job"); - } }, FINISH_LINGER_TICKS); } - if (sender.isPlayer() && claim != null) { - HudSurface surface = claim.resolve(); - if (surface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:chunk-job"); - sender.sendAction(IrisLanguage.text( - ok ? RuntimeProgressMessages.CHUNK_ACTION_DONE : RuntimeProgressMessages.CHUNK_ACTION_FAILED, - MessageArgument.trusted("bar", progressBar(1.0D)), - MessageArgument.trusted("summary", summary) - )); - } else if (surface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:chunk-job", IrisLanguage.text( - ok ? RuntimeProgressMessages.CHUNK_ACTION_DONE : RuntimeProgressMessages.CHUNK_ACTION_FAILED, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("summary", summary) - ), 1.0D, ok ? BarColor.GREEN : BarColor.RED, BarStyle.SOLID, 4000L); - } + if (sender.isPlayer()) { + sender.sendAction(IrisLanguage.text( + ok ? RuntimeProgressMessages.CHUNK_ACTION_DONE : RuntimeProgressMessages.CHUNK_ACTION_FAILED, + MessageArgument.trusted("bar", progressBar(1.0D)), + MessageArgument.trusted("summary", summary) + )); } sender.sendMessage(IrisLanguage.text( ok ? RuntimeProgressMessages.CHUNK_COMPLETE : RuntimeProgressMessages.CHUNK_FAILED, 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 24c092136..1c3f728b9 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 @@ -230,7 +230,15 @@ public final class StudioOpenCoordinator { IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); if (!request.retainOnFailure()) { updateStage(request, "cleanup", 1.00D); - if (requiresDeferredEntryCleanup(entryLoadFuture)) { + if (LifecycleOperationCoordinator.get() + .active(LifecycleOperationCoordinator.Domain.SERVER_LIFECYCLE) + .isPresent()) { + deferFailedOpenCleanupToRestart( + provider, + request.worldName(), + world, + entryLoadFuture); + } else if (requiresDeferredEntryCleanup(entryLoadFuture)) { deferFailedOpenCleanup( entryLoadFuture, provider, @@ -259,6 +267,33 @@ public final class StudioOpenCoordinator { } } + private void deferFailedOpenCleanupToRestart( + PlatformChunkGenerator provider, + String worldName, + World world, + CompletableFuture entryLoadFuture + ) { + if (provider != null || world != null || transientWorldStorageExists(worldName)) { + queueStartupCleanup( + worldName, + new IllegalStateException("Studio cleanup deferred across the queued server restart.")); + } + entryLoads.release(worldName, entryLoadFuture); + } + + private boolean transientWorldStorageExists(String worldName) { + if (worldName == null || worldName.isBlank()) { + return false; + } + try { + File root = IrisWorldStorage.requireSafeManagedDimensionRoot( + IrisWorldStorage.managedKeyFromName(worldName)); + return Files.exists(root.toPath(), LinkOption.NOFOLLOW_LINKS); + } catch (RuntimeException failure) { + return true; + } + } + private long logStudioPhase(StudioOpenRequest request, String phase, long t, long openStart) { long now = System.nanoTime(); IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms", diff --git a/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java b/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java index f1170c428..ba9165ce7 100644 --- a/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java +++ b/core/src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java @@ -9,16 +9,11 @@ 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.boss.BarColor; import org.bukkit.boss.BarStyle; import org.bukkit.entity.Player; -import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -30,7 +25,6 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr private static final int INDETERMINATE_SEGMENT_WIDTH = 5; private static final int HUD_PULSE_TICKS = 5; private static final int HUD_TERMINAL_TICKS = 60; - private static final long HUD_CLAIM_TTL_MILLIS = HUD_TERMINAL_TICKS * 50L + 1_000L; private static final long ACTION_INTERVAL_MILLIS = 250L; private static final long CHAT_INTERVAL_MILLIS = 5_000L; private static final int CHAT_PERCENT_STEP = 10; @@ -47,7 +41,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr private final String hudLaneId; private PackDownloader.DownloadPhase phase; private PackDownloader.DownloadProgress latestProgress; - private HudSlotClaim hudClaim; + private boolean hudActive; private long transferredBytes; private long transferElapsedMillis; private long phaseStartedMillis; @@ -93,12 +87,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr if (player == null || !BukkitPlatform.hasHud()) { return; } - hudClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest( - hudLaneId, - HudPriority.PROGRESS, - HUD_CLAIM_TTL_MILLIS, - List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR) - )); + hudActive = true; int scheduledTaskId = J.ar(this::pulseHud, HUD_PULSE_TICKS); pulseTaskId = scheduledTaskId; if (finished || hudDisabled) { @@ -237,9 +226,8 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr private void pulseHud() { HudSnapshot snapshot; - HudSlotClaim claim; synchronized (this) { - if (finished || hudDisabled || hudClaim == null) { + if (finished || hudDisabled || !hudActive) { stopPulse(); return; } @@ -249,33 +237,25 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr } lastActionMillis = now; snapshot = hudSnapshot(now); - claim = hudClaim; } - boolean scheduled = J.runEntity(player, () -> renderHudPulse(claim, snapshot)); + boolean scheduled = J.runEntity(player, () -> renderHudPulse(snapshot)); if (!scheduled) { disableHud(null); } } - private void renderHudPulse(HudSlotClaim claim, HudSnapshot snapshot) { + private void renderHudPulse(HudSnapshot snapshot) { try { - HudSurface surface = claim.resolve(); - if (surface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(player, hudLaneId); - sender.sendAction(snapshot.line()); - } else if (surface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show( - player, - hudLaneId, - snapshot.line(), - snapshot.progress(), - BarColor.BLUE, - BarStyle.SEGMENTED_20, - 1_500L - ); - } else { - BukkitPlatform.hudLanes().hide(player, hudLaneId); - } + BukkitPlatform.hudLanes().show( + player, + hudLaneId, + snapshot.line(), + snapshot.progress(), + BarColor.BLUE, + BarStyle.SEGMENTED_20, + 1_500L + ); + sender.sendAction(snapshot.line()); } catch (RuntimeException failure) { disableHud(failure); } @@ -315,9 +295,9 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr } private synchronized void deliverTerminalHud(String message, BarColor color, double progress) { - HudSlotClaim claim = hudClaim; - hudClaim = null; - if (player == null || claim == null || hudDisabled) { + boolean active = hudActive; + hudActive = false; + if (player == null || !active || hudDisabled) { return; } long now = System.currentTimeMillis(); @@ -326,25 +306,20 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr int delayTicks = (int) Math.ceil(delayMillis / 50.0D); lastActionMillis = now + delayMillis; AtomicBoolean cleaned = new AtomicBoolean(); - Runnable cleanup = () -> releaseHudClaim(cleaned, claim); - Runnable retiredCleanup = () -> retireHudClaim(cleaned, claim); + Runnable cleanup = () -> releaseHudLane(cleaned); + Runnable retiredCleanup = () -> retireHudLane(cleaned); Runnable display = () -> { try { - HudSurface surface = claim.resolve(); - if (surface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(player, hudLaneId); - sender.sendAction(message); - } else if (surface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show( - player, - hudLaneId, - message, - progress, - color, - BarStyle.SOLID, - 4_000L - ); - } + BukkitPlatform.hudLanes().show( + player, + hudLaneId, + message, + progress, + color, + BarStyle.SOLID, + 4_000L + ); + sender.sendAction(message); } finally { if (!J.runEntity(player, cleanup, HUD_TERMINAL_TICKS, retiredCleanup)) { retiredCleanup.run(); @@ -359,43 +334,41 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr } private void disableHud(Throwable failure) { - HudSlotClaim claim; + boolean active; synchronized (this) { if (hudDisabled) { return; } hudDisabled = true; stopPulse(); - claim = hudClaim; - hudClaim = null; + active = hudActive; + hudActive = false; } if (failure != null) { IrisLogging.reportError("Pack download HUD disabled after a delivery failure.", failure); } - if (player != null && claim != null) { + if (player != null && active) { AtomicBoolean cleaned = new AtomicBoolean(); - Runnable cleanup = () -> releaseHudClaim(cleaned, claim); - Runnable retiredCleanup = () -> retireHudClaim(cleaned, claim); + Runnable cleanup = () -> releaseHudLane(cleaned); + Runnable retiredCleanup = () -> retireHudLane(cleaned); if (!J.runEntity(player, cleanup, 0, retiredCleanup)) { retiredCleanup.run(); } } } - private void releaseHudClaim(AtomicBoolean cleaned, HudSlotClaim claim) { + private void releaseHudLane(AtomicBoolean cleaned) { if (!cleaned.compareAndSet(false, true)) { return; } BukkitPlatform.hudLanes().hide(player, hudLaneId); - claim.release(); } - private void retireHudClaim(AtomicBoolean cleaned, HudSlotClaim claim) { + private void retireHudLane(AtomicBoolean cleaned) { if (!cleaned.compareAndSet(false, true)) { return; } BukkitPlatform.hudLanes().retire(playerId, hudLaneId); - claim.retire(); } private synchronized void stopPulse() { 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 816670436..8df72be85 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 @@ -184,7 +184,9 @@ public class StudioSVC implements IrisService { boolean replaceExisting ) { if (J.isPrimaryThread()) { - sender.sendMessage("Iris refused to copy a pack on the Bukkit primary thread."); + sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD + )); return null; } String dimensionKey = dimension.getLoadKey(); @@ -193,7 +195,11 @@ public class StudioSVC implements IrisService { source = resolveSafePackSource(dimension.getLoader().getDataFolder()); } catch (IOException e) { IrisLogging.reportError("Failed to inspect source dimension pack '" + dimensionKey + "'.", e); - sender.sendMessage("Failed to install studio pack '" + dimensionKey + "': " + errorDetail(e)); + sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED, + MessageArgument.untrusted("dimension", dimensionKey), + MessageArgument.untrusted("error", errorDetail(e)) + )); return null; } Path target = folder.toPath().toAbsolutePath().normalize(); @@ -282,7 +288,11 @@ public class StudioSVC implements IrisService { } } IrisLogging.reportError("Failed to install dimension pack '" + dimensionKey + "' into " + folder.getPath(), e); - sender.sendMessage("Failed to install studio pack '" + dimensionKey + "': " + errorDetail(e)); + sender.sendMessage(IrisLanguage.text( + BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED, + MessageArgument.untrusted("dimension", dimensionKey), + MessageArgument.untrusted("error", errorDetail(e)) + )); return null; } finally { if (validationMutation != null) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java b/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java index 9fc86f846..577762ee8 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisConverter.java @@ -7,10 +7,6 @@ import art.arcane.iris.engine.object.IrisObject; import art.arcane.volmlib.util.data.Varint; import art.arcane.iris.util.common.format.C; 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.nbt.io.NBTUtil; import art.arcane.volmlib.util.nbt.io.NamedTag; import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; @@ -24,8 +20,6 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import org.apache.commons.io.FileUtils; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; import java.io.ByteArrayInputStream; import java.io.DataInputStream; @@ -38,7 +32,6 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import art.arcane.iris.core.localization.BukkitRuntimeMessages; import art.arcane.iris.core.localization.IrisLanguage; @@ -84,41 +77,18 @@ public class IrisConverter { int mv = objW * objH * objD; AtomicInteger v = new AtomicInteger(0); boolean reportProgress = mv > 2_000_000 && sender.isPlayer(); - HudSlotClaim titleClaim = reportProgress - ? BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE))) - : null; - HudSlotClaim barClaim = reportProgress - ? BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR))) - : null; // try/finally over the whole decode: a throw must never leak the - // self-rescheduling progress task or the HUD claims. + // self-rescheduling progress task or the HUD lane. try { if (mv > 2_000_000) { largeObject = true; IrisLogging.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); IrisLogging.info(C.GRAY + "- It may take a while"); if (reportProgress) { - AtomicLong lastResolveMs = new AtomicLong(0L); i = J.ar(() -> { - long now = System.currentTimeMillis(); - if (now - lastResolveMs.get() >= 250L) { - lastResolveMs.set(now); - titleClaim.resolve(); - barClaim.resolve(); - } double conversionProgress = (double) v.get() / mv; - HudSurface barSurface = barClaim.granted(); - sender.sendProgress( - conversionProgress, - IrisLanguage.text(RuntimeUiMessages.CONVERTING), - titleClaim.granted(), - barSurface - ); - if (barSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:job", IrisLanguage.text(RuntimeUiMessages.CONVERTING) + " " + Form.pc(conversionProgress, 0), conversionProgress, BarColor.BLUE, BarStyle.SOLID, 4000L); - } else if (barSurface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); - } + sender.sendProgress(conversionProgress, IrisLanguage.text(RuntimeUiMessages.CONVERTING)); + BukkitPlatform.showProgressLane(sender.player(), "iris:job", IrisLanguage.text(RuntimeUiMessages.CONVERTING) + " " + Form.pc(conversionProgress, 0), conversionProgress, 4000L); }, 0); } } @@ -154,11 +124,8 @@ public class IrisConverter { } finally { if (i != -1) J.car(i); - if (titleClaim != null) { - titleClaim.release(); - } - if (barClaim != null) { - barClaim.release(); + if (reportProgress) { + sender.sendAction(" "); BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); } } diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 5bb0c3caf..b17b1e4d2 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -51,10 +51,6 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.exceptions.IrisException; import art.arcane.iris.util.common.format.C; 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 art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.iris.util.common.plugin.VolmitSender; @@ -84,7 +80,6 @@ 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; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.IntSupplier; @@ -163,6 +158,9 @@ public class IrisCreator { if (Bukkit.isPrimaryThread()) { throw new IrisException("You cannot invoke create() on the main thread."); } + if (sender == null) { + sender = BukkitPlatform.console(); + } NamespacedKey worldKey; try { worldKey = IrisWorldStorage.managedKeyFromName(name); @@ -170,15 +168,20 @@ public class IrisCreator { throw new IrisException(e.getMessage(), e); } name = IrisWorldStorage.logicalName(worldKey); + WorldCreationProgressReporter creationReporter = !studio && !benchmark + ? WorldCreationProgressReporter.start(sender, name) + : null; LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get(); LifecycleOperationCoordinator.Lease worldLease = null; try { reportStudioProgress(0.02D, "resolve_dimension"); + reportCreationProgress(creationReporter, 0.02D, "resolve_dimension"); IrisDimension resolvedDimension = IrisToolbelt.getDimension(dimension()); if (resolvedDimension == null) { throw new IrisException("Dimension cannot be found for id " + dimension()); } + reportCreationProgress(creationReporter, 0.06D, "validate_pack"); IrisStartupValidation.requireWorldCreationReady(); PackValidationRegistry.requireLoadable( resolvedDimension.getLoader().getDataFolder().getName()); @@ -186,9 +189,31 @@ public class IrisCreator { LifecycleOperationCoordinator.Domain.WORLD_MUTATION, LifecycleOperationCoordinator.OperationKind.WORLD_CREATE, worldKey.toString()); - return createReserved(worldKey, resolvedDimension); + World world = createReserved(worldKey, resolvedDimension, creationReporter); + reportCreationProgress(creationReporter, 1.0D, "complete"); + if (creationReporter != null) { + creationReporter.succeed(); + } + return world; } catch (LifecycleOperationCoordinator.BusyException e) { + if (creationReporter != null) { + creationReporter.fail(); + } throw new IrisException(e.getMessage(), e); + } catch (Throwable e) { + if (creationReporter != null) { + creationReporter.fail(); + } + if (e instanceof IrisException irisException) { + throw irisException; + } + if (e instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (e instanceof Error error) { + throw error; + } + throw new IrisException("Failed to create world \"" + name + "\".", e); } finally { if (worldLease != null) { worldLease.close(); @@ -196,7 +221,11 @@ public class IrisCreator { } } - private World createReserved(NamespacedKey worldKey, IrisDimension resolvedDimension) throws IrisException { + private World createReserved( + NamespacedKey worldKey, + IrisDimension resolvedDimension, + WorldCreationProgressReporter creationReporter + ) throws IrisException { File dimensionRoot; File storageRoot; try { @@ -213,16 +242,13 @@ public class IrisCreator { if (Files.exists(storageRoot.toPath()) || WorldIdentity.resolve(worldKey).isPresent()) { throw new IrisException("World \"" + name + "\" already exists or is loaded."); } - if (sender == null) { - sender = BukkitPlatform.console(); - } - World world = null; boolean bukkitRegistered = false; PlatformChunkGenerator stagedGenerator = null; try { reportStudioProgress(0.08D, "resolve_dimension"); reportStudioProgress(0.16D, "prepare_world_pack"); + reportCreationProgress(creationReporter, 0.10D, "install_datapacks"); DatapackInstallResult datapackResult = prepareDatapacks(resolvedDimension); if (!datapackResult.succeeded()) { throw new IrisException("Failed to compile datapacks for dimension \"" + dimension() + "\"."); @@ -235,6 +261,7 @@ public class IrisCreator { IrisDimension installedDimension = resolvedDimension; if (!studio() || benchmark) { + reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack"); installedDimension = IrisServices.get(StudioSVC.class) .installIntoWorld(sender, resolvedDimension, dimensionRoot); if (installedDimension == null) { @@ -249,6 +276,7 @@ public class IrisCreator { } reportStudioProgress(0.28D, "install_datapacks"); + reportCreationProgress(creationReporter, 0.36D, "prepare_generator"); AtomicDouble pp = new AtomicDouble(0); AtomicBoolean done = new AtomicBoolean(false); long generatorPrepareStart = System.nanoTime(); @@ -267,12 +295,10 @@ public class IrisCreator { throw new IrisException("Access is null. Something bad happened."); } stagedGenerator = access; - HudSlotClaim createClaim = !benchmark && studioProgressConsumer == null && sender.isPlayer() - ? openLoaderClaim("iris:world-create") - : null; - AtomicInteger createProgressTask = startCreateProgressReporter(access, done, createClaim); + AtomicInteger createProgressTask = startCreateProgressReporter(access, done, creationReporter); reportStudioProgress(0.46D, "create_world"); + reportCreationProgress(creationReporter, 0.44D, "create_world"); long nmsStartNanos = System.nanoTime(); try { WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE; @@ -283,7 +309,6 @@ public class IrisCreator { } catch (Throwable e) { done.set(true); cancelRepeatingTask(createProgressTask); - releaseLoaderClaim(createClaim, "iris:world-create"); if (e instanceof TimeoutException) { ServerConfigurator.restart("World creation timed out for \"" + name + "\"."); } @@ -302,8 +327,8 @@ public class IrisCreator { done.set(true); cancelRepeatingTask(createProgressTask); - releaseLoaderClaim(createClaim, "iris:world-create"); reportStudioProgress(0.86D, "create_world"); + reportCreationProgress(creationReporter, 0.84D, "register_world"); if (!studio && !benchmark) { BukkitWorldConfiguration.register( @@ -327,6 +352,7 @@ public class IrisCreator { throw e; } } + reportCreationProgress(creationReporter, 0.92D, "teleport_player"); awaitSenderTeleport(world); if (pregen != null) { @@ -336,20 +362,26 @@ public class IrisCreator { .whenDone(() -> ff.complete(true)); AtomicBoolean dx = new AtomicBoolean(false); - HudSlotClaim pregenClaim = sender.isPlayer() ? openLoaderClaim("iris:pregen") : null; - AtomicInteger pregenProgressTask = startPregenProgressReporter(pp, dx, pregenClaim); + boolean pregenHud = creationReporter == null && sender.isPlayer(); + AtomicInteger pregenProgressTask = startPregenProgressReporter( + pp, + dx, + pregenHud, + creationReporter + ); try { ff.get(); dx.set(true); cancelRepeatingTask(pregenProgressTask); - releaseLoaderClaim(pregenClaim, "iris:pregen"); + hidePregenLane(pregenHud); } catch (Throwable e) { dx.set(true); cancelRepeatingTask(pregenProgressTask); - releaseLoaderClaim(pregenClaim, "iris:pregen"); + hidePregenLane(pregenHud); IrisLogging.reportError(e); } } + reportCreationProgress(creationReporter, 0.99D, "finalize"); return world; } catch (Throwable failure) { rollbackWorldCreation(worldKey, world, stagedGenerator, storageRoot, bukkitRegistered, failure); @@ -470,6 +502,16 @@ public class IrisCreator { } } + private void reportCreationProgress( + WorldCreationProgressReporter reporter, + double progress, + String stage + ) { + if (reporter != null) { + reporter.update(progress, stage); + } + } + private void reportStudioTiming(String phase, long startedAtNanos) { BiConsumer consumer = studioTimingConsumer; if (consumer == null) { @@ -498,7 +540,11 @@ public class IrisCreator { return ServerConfigurator.installDataPacksIfChanged(true, studioTimingConsumer); } - private AtomicInteger startCreateProgressReporter(PlatformChunkGenerator access, AtomicBoolean done, HudSlotClaim claim) { + private AtomicInteger startCreateProgressReporter( + PlatformChunkGenerator access, + AtomicBoolean done, + WorldCreationProgressReporter creationReporter + ) { AtomicInteger taskId = new AtomicInteger(-1); if (benchmark) { return taskId; @@ -510,10 +556,9 @@ public class IrisCreator { } return access.getEngine().getGenerated(); }; - AtomicLong lastResolveMs = new AtomicLong(0L); access.getSpawnChunks().whenComplete((required, throwable) -> { if (throwable != null) { - IrisLogging.reportError("Failed to resolve studio spawn chunk target for world \"" + name() + "\".", throwable); + IrisLogging.reportError("Failed to resolve spawn chunk target for world \"" + name() + "\".", throwable); return; } @@ -521,7 +566,7 @@ public class IrisCreator { return; } - int interval = studioProgressConsumer != null || sender.isPlayer() ? 1 : 20; + int interval = studioProgressConsumer != null || creationReporter != null && sender.isPlayer() ? 1 : 20; taskId.set(J.ar(() -> { if (done.get()) { cancelRepeatingTask(taskId); @@ -537,57 +582,27 @@ public class IrisCreator { double progress = (double) generated / required; if (studioProgressConsumer != null) { reportStudioProgress(0.40D + (0.42D * progress), "create_world"); - return; } - int percent = (int) Math.round(progress * 100.0D); - int remaining = required - generated; - if (sender.isPlayer() && claim != null) { - HudSurface surface = resolveThrottled(claim, lastResolveMs); - if (surface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:world-create"); - int barWidth = 44; - int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * barWidth); - StringBuilder bar = new StringBuilder(barWidth * 3 + 4); - bar.append(C.DARK_GRAY).append("["); - for (int bi = 0; bi < barWidth; bi++) { - bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|"); - } - bar.append(C.DARK_GRAY).append("]"); - sender.sendAction(IrisLanguage.text( - RuntimeProgressMessages.WORLD_CREATE_ACTION, - MessageArgument.trusted("bar", bar.toString()), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("generated", Form.f(generated)), - MessageArgument.trusted("required", Form.f(required)) - )); - } else if (surface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:world-create", IrisLanguage.text( - RuntimeProgressMessages.WORLD_CREATE_ACTION, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("generated", Form.f(generated)), - MessageArgument.trusted("required", Form.f(required)) - ), progress, BarColor.GREEN, BarStyle.SOLID, 4000L); - } - return; + if (creationReporter != null) { + creationReporter.update( + 0.44D + (0.38D * progress), + "create_world", + C.DARK_GRAY + " (" + Form.f(generated) + "/" + Form.f(required) + " chunks)" + ); } - - sender.sendMessage(IrisLanguage.text( - RuntimeProgressMessages.WORLD_CREATE_CONSOLE, - MessageArgument.trusted("percent", percent), - MessageArgument.trusted("generated", Form.f(generated)), - MessageArgument.trusted("required", Form.f(required)), - MessageArgument.trusted("remaining", remaining) - )); }, interval)); }); return taskId; } - private AtomicInteger startPregenProgressReporter(AtomicDouble progress, AtomicBoolean done, HudSlotClaim claim) { + private AtomicInteger startPregenProgressReporter( + AtomicDouble progress, + AtomicBoolean done, + boolean showLoaderHud, + WorldCreationProgressReporter creationReporter + ) { AtomicInteger taskId = new AtomicInteger(-1); - AtomicLong lastResolveMs = new AtomicLong(0L); int interval = sender.isPlayer() ? 1 : 20; taskId.set(J.ar(() -> { if (done.get()) { @@ -597,30 +612,29 @@ public class IrisCreator { double p = progress.get(); int percent = (int) Math.round(p * 100.0D); - if (sender.isPlayer() && claim != null) { - HudSurface surface = resolveThrottled(claim, lastResolveMs); - if (surface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:pregen"); - int barWidth = 44; - int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth); - StringBuilder bar = new StringBuilder(barWidth * 3 + 4); - bar.append(C.DARK_GRAY).append("["); - for (int bi = 0; bi < barWidth; bi++) { - bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|"); - } - bar.append(C.DARK_GRAY).append("]"); - sender.sendAction(IrisLanguage.text( - RuntimeProgressMessages.WORLD_PREGEN_ACTION, - MessageArgument.trusted("bar", bar.toString()), - MessageArgument.trusted("percent", percent) - )); - } else if (surface == HudSurface.BOSS_BAR) { - BukkitPlatform.hudLanes().show(sender.player(), "iris:pregen", IrisLanguage.text( - RuntimeProgressMessages.WORLD_PREGEN_ACTION, - MessageArgument.trusted("bar", ""), - MessageArgument.trusted("percent", percent) - ), p, BarColor.GREEN, BarStyle.SOLID, 4000L); + if (creationReporter != null) { + creationReporter.update(0.94D + (0.05D * p), "pregenerate"); + return; + } + if (showLoaderHud) { + BukkitPlatform.hudLanes().show(sender.player(), "iris:pregen", IrisLanguage.text( + RuntimeProgressMessages.WORLD_PREGEN_ACTION, + MessageArgument.trusted("bar", ""), + MessageArgument.trusted("percent", percent) + ), p, BarColor.GREEN, BarStyle.SOLID, 4000L); + int barWidth = 44; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth); + StringBuilder bar = new StringBuilder(barWidth * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int bi = 0; bi < barWidth; bi++) { + bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|"); } + bar.append(C.DARK_GRAY).append("]"); + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.WORLD_PREGEN_ACTION, + MessageArgument.trusted("bar", bar.toString()), + MessageArgument.trusted("percent", percent) + )); return; } @@ -632,31 +646,12 @@ public class IrisCreator { return taskId; } - private HudSlotClaim openLoaderClaim(String purpose) { - return BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest( - purpose, - HudPriority.PROGRESS, - 1200L, - List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR) - )); - } - - private void releaseLoaderClaim(HudSlotClaim claim, String laneId) { - if (claim == null) { + private void hidePregenLane(boolean shown) { + if (!shown) { return; } - claim.release(); - BukkitPlatform.hudLanes().hide(sender.player(), laneId); - } - - private static HudSurface resolveThrottled(HudSlotClaim claim, AtomicLong lastResolveMillis) { - long now = System.currentTimeMillis(); - if (now - lastResolveMillis.get() >= 250L) { - lastResolveMillis.set(now); - return claim.resolve(); - } - HudSurface granted = claim.granted(); - return granted == null ? claim.resolve() : granted; + BukkitPlatform.hudLanes().hide(sender.player(), "iris:pregen"); + sender.sendAction(" "); } private void cancelRepeatingTask(AtomicInteger taskId) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java b/core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java new file mode 100644 index 000000000..6a54b8a1a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java @@ -0,0 +1,399 @@ +/* + * 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.tools; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.spi.IrisLogging; +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.localization.MessageArgument; +import art.arcane.volmlib.util.localization.TextKey; +import org.bukkit.Bukkit; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +final class WorldCreationProgressReporter { + private static final int PLAYER_BAR_WIDTH = 44; + private static final int CONSOLE_BAR_WIDTH = 20; + private static final long CONSOLE_UPDATE_INTERVAL_MILLIS = 1500L; + + private final VolmitSender sender; + private final String worldName; + private final long startedAtMillis; + private final AtomicReference stage; + private final AtomicReference detail; + private final AtomicReference progress; + private final AtomicBoolean complete; + private final AtomicBoolean failed; + private final AtomicBoolean terminalRendered; + private final AtomicBoolean playerRenderQueued; + private final AtomicInteger taskId; + private final AtomicLong nextConsoleUpdate; + private volatile BossBar bossBar; + private volatile boolean hudDisabled; + + private WorldCreationProgressReporter(VolmitSender sender, String worldName) { + this.sender = sender; + this.worldName = worldName; + this.startedAtMillis = System.currentTimeMillis(); + this.stage = new AtomicReference<>("resolve_dimension"); + this.detail = new AtomicReference<>(""); + this.progress = new AtomicReference<>(0.01D); + this.complete = new AtomicBoolean(false); + this.failed = new AtomicBoolean(false); + this.terminalRendered = new AtomicBoolean(false); + this.playerRenderQueued = new AtomicBoolean(false); + this.taskId = new AtomicInteger(-1); + this.nextConsoleUpdate = new AtomicLong(0L); + + this.bossBar = null; + this.hudDisabled = false; + } + + static WorldCreationProgressReporter start(VolmitSender sender, String worldName) { + WorldCreationProgressReporter reporter = new WorldCreationProgressReporter(sender, worldName); + if (sender.isPlayer() && sender.player() != null) { + try { + J.sfut(reporter::initializePlayerHud).get(5L, TimeUnit.SECONDS); + } catch (Throwable failure) { + reporter.hudDisabled = true; + J.runGlobal(reporter::releaseHud); + IrisLogging.reportError("Failed to initialize world creation progress HUD for \"" + + worldName + "\".", failure); + } + } + reporter.taskId.set(J.ar(reporter::tick, 3)); + return reporter; + } + + private void initializePlayerHud() { + if (hudDisabled) { + return; + } + bossBar = Bukkit.createBossBar( + IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_WORKING, + MessageArgument.untrusted("world", worldName) + ), + BarColor.BLUE, + BarStyle.SEGMENTED_20 + ); + bossBar.setProgress(0.01D); + bossBar.addPlayer(sender.player()); + bossBar.setVisible(true); + } + + void update(double progress, String stage) { + update(progress, stage, ""); + } + + void update(double progress, String stage, String detail) { + this.progress.set(clampProgress(progress)); + if (stage != null && !stage.isBlank()) { + this.stage.set(stage); + } + this.detail.set(detail == null ? "" : detail); + } + + void succeed() { + progress.set(1.0D); + stage.set("complete"); + detail.set(""); + complete.set(true); + requestTerminalRender(); + } + + void fail() { + failed.set(true); + complete.set(true); + requestTerminalRender(); + } + + private void tick() { + double currentProgress = complete.get() && !failed.get() + ? 1.0D + : Math.min(0.99D, clampProgress(progress.get())); + String currentStage = IrisLanguage.text(stageKey(stage.get())); + String currentDetail = detail.get(); + int percent = (int) Math.round(currentProgress * 100.0D); + long elapsed = System.currentTimeMillis() - startedAtMillis; + + if (complete.get()) { + cancel(); + if (!terminalRendered.compareAndSet(false, true)) { + return; + } + renderTerminal(currentProgress, currentStage, currentDetail, percent, elapsed); + return; + } + + if (sender.isPlayer() && sender.player() != null) { + if (hasPlayerHud()) { + schedulePlayerRender(() -> renderPlayerProgress( + currentProgress, + currentStage, + currentDetail, + percent, + elapsed + )); + } else { + schedulePlayerRender(() -> sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + ))); + } + return; + } + + long now = System.currentTimeMillis(); + if (now >= nextConsoleUpdate.get()) { + sender.sendMessage(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_CONSOLE, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("bar", buildConsoleBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + nextConsoleUpdate.set(now + CONSOLE_UPDATE_INTERVAL_MILLIS); + } + } + + private void renderPlayerProgress( + double currentProgress, + String currentStage, + String currentDetail, + int percent, + long elapsed + ) { + bossBar.setProgress(currentProgress); + bossBar.setTitle(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_PROGRESS, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage) + )); + + sender.sendAction(IrisLanguage.text( + RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) + )); + } + + private void renderTerminal( + double currentProgress, + String currentStage, + String currentDetail, + int percent, + long elapsed + ) { + if (sender.isPlayer() && sender.player() != null) { + if (hasPlayerHud()) { + schedulePlayerTerminalRender(() -> renderPlayerTerminal( + currentProgress, + currentStage, + currentDetail, + percent, + elapsed + )); + } else { + schedulePlayerTerminalRender(() -> sender.sendAction(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED + : RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + ))); + } + return; + } + + sender.sendMessage(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_CONSOLE_FAILED + : RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_CONSOLE_READY, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); + } + + private void renderPlayerTerminal( + double currentProgress, + String currentStage, + String currentDetail, + int percent, + long elapsed + ) { + bossBar.setProgress(currentProgress); + bossBar.setColor(failed.get() ? BarColor.RED : BarColor.GREEN); + bossBar.setTitle(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_FAILED + : RuntimeProgressMessages.WORLD_CREATE_BOSSBAR_READY, + MessageArgument.untrusted("world", worldName), + MessageArgument.trusted("percent", percent) + )); + + sender.sendAction(IrisLanguage.text( + failed.get() + ? RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_FAILED + : RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION_READY, + MessageArgument.trusted("bar", buildPlayerBar(currentProgress)), + MessageArgument.trusted("percent", percent), + MessageArgument.trusted("stage", currentStage), + MessageArgument.trusted("detail", currentDetail), + MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) + )); + + Runnable cleanup = () -> { + bossBar.removeAll(); + bossBar.setVisible(false); + }; + J.runEntity(sender.player(), cleanup, 60, cleanup); + } + + private boolean hasPlayerHud() { + return !hudDisabled + && sender.isPlayer() + && sender.player() != null + && bossBar != null; + } + + private void schedulePlayerRender(Runnable render) { + if (!playerRenderQueued.compareAndSet(false, true)) { + return; + } + Runnable guardedRender = () -> { + try { + render.run(); + } finally { + playerRenderQueued.set(false); + } + }; + if (J.runEntity(sender.player(), guardedRender)) { + return; + } + playerRenderQueued.set(false); + hudDisabled = true; + J.runGlobal(this::releaseHud); + } + + private void schedulePlayerTerminalRender(Runnable render) { + if (J.runEntity(sender.player(), render)) { + return; + } + hudDisabled = true; + J.runGlobal(this::releaseHud); + } + + private void releaseHud() { + BossBar activeBossBar = bossBar; + if (activeBossBar != null) { + activeBossBar.removeAll(); + activeBossBar.setVisible(false); + } + } + + private void cancel() { + int scheduledTaskId = taskId.getAndSet(-1); + if (scheduledTaskId >= 0) { + J.car(scheduledTaskId); + } + } + + private void requestTerminalRender() { + if (!J.runGlobal(this::tick)) { + cancel(); + } + } + + static double clampProgress(double progress) { + return Math.max(0.0D, Math.min(1.0D, progress)); + } + + static String buildPlayerBar(double progress) { + return buildBar(progress, PLAYER_BAR_WIDTH, true); + } + + static String buildConsoleBar(double progress) { + return buildBar(progress, CONSOLE_BAR_WIDTH, false); + } + + private static String buildBar(double progress, int width, boolean colored) { + int filled = (int) Math.round(clampProgress(progress) * width); + StringBuilder bar = new StringBuilder(colored ? width * 3 + 4 : width + 2); + if (colored) { + bar.append(C.DARK_GRAY); + } + bar.append("["); + for (int index = 0; index < width; index++) { + if (colored) { + bar.append(index < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } else { + bar.append(index < filled ? "#" : "-"); + } + } + if (colored) { + bar.append(C.DARK_GRAY); + } + return bar.append("]").toString(); + } + + static TextKey stageKey(String stage) { + if (stage == null || stage.isBlank()) { + return RuntimeProgressMessages.WORLD_CREATE_STAGE_INITIALIZING; + } + return switch (stage) { + case "resolve_dimension" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_RESOLVE_DIMENSION; + case "validate_pack" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_VALIDATE_PACK; + case "prepare_world_pack" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_PREPARE_WORLD_PACK; + case "install_datapacks" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_INSTALL_DATAPACKS; + case "prepare_generator" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_PREPARE_GENERATOR; + case "create_world" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_CREATE_WORLD; + case "register_world" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_REGISTER_WORLD; + case "teleport_player" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_TELEPORT_PLAYER; + case "pregenerate" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_PREGENERATE; + case "finalize" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_FINALIZE; + case "complete" -> RuntimeProgressMessages.WORLD_CREATE_STAGE_COMPLETE; + default -> RuntimeProgressMessages.WORLD_CREATE_STAGE_INITIALIZING; + }; + } +} 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 5c24626f2..b752e8910 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 @@ -39,7 +39,15 @@ final class DecoratorCore { private static final long SEED_OFFSET = 29356788L; private static final long PART_FACTOR = 10439677L; + private static final String WEEPING_VINES = "minecraft:weeping_vines"; + private static final String WEEPING_VINES_PLANT = "minecraft:weeping_vines_plant"; + private static final String TWISTING_VINES = "minecraft:twisting_vines"; + private static final String TWISTING_VINES_PLANT = "minecraft:twisting_vines_plant"; private static final boolean BUKKIT_PRESENT = detectBukkit(); + private static volatile PlatformBlockState weepingVines; + private static volatile PlatformBlockState weepingVinesPlant; + private static volatile PlatformBlockState twistingVines; + private static volatile PlatformBlockState twistingVinesPlant; private static boolean detectBukkit() { try { @@ -220,6 +228,7 @@ final class DecoratorCore { return; } + block = stackedVineBlock(block, stack, 0); data.set(x, targetY, z, block); return; } @@ -259,6 +268,7 @@ final class DecoratorCore { bd = dripstoneBlock(stack, i, "up"); } + bd = stackedVineBlock(bd, stack, i); data.set(x, height + 1 + i, z, bd); } } @@ -280,6 +290,7 @@ final class DecoratorCore { if (block == null) { return; } + block = stackedVineBlock(block, stack, 0); data.set(x, height, z, fixFacesForHunk(block, data, x, z, realX, height, realZ, mantle)); return; } @@ -303,6 +314,7 @@ final class DecoratorCore { bd = dripstoneBlock(stack, i, "down"); } + bd = stackedVineBlock(bd, stack, i); if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) { break; } @@ -367,6 +379,7 @@ final class DecoratorCore { if (bd == null) { break; } + bd = stackedVineBlock(bd, stack, i); data.set(xf, h, zf, bd); placed++; } @@ -431,6 +444,9 @@ final class DecoratorCore { } static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) { + if (!B.canPlaceOnto(decorator, surface)) { + return false; + } if (!BUKKIT_PRESENT) { DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness(); return sturdiness == null ? B.isSolid(surface) : sturdiness.canGoOn(surface); @@ -438,10 +454,10 @@ final class DecoratorCore { return ((BlockData) surface.nativeHandle()).isFaceSturdy(BlockFace.UP, BlockSupport.FULL); } - static boolean isValidShorelineSupport(IrisDecorator decorator, PlatformBlockState surface) { + static boolean isValidShorelineSupport(IrisDecorator decorator, PlatformBlockState decorant, PlatformBlockState surface) { return surface != null && B.isSolid(surface) - && (decorator.isForcePlace() || canGoOn(null, surface)); + && (decorator.isForcePlace() || canGoOn(decorant, surface)); } static boolean canReplaceStackTarget(PlatformBlockState state, boolean allowFluid) { @@ -503,4 +519,28 @@ final class DecoratorCore { } return dripstoneDown[thIdx]; } + + static String stackedVineKey(PlatformBlockState state, int stack, int index) { + String material = IrisProceduralBlocks.materialKey(state); + boolean tip = index == stack - 1; + return switch (material) { + case WEEPING_VINES, WEEPING_VINES_PLANT -> tip ? WEEPING_VINES : WEEPING_VINES_PLANT; + case TWISTING_VINES, TWISTING_VINES_PLANT -> tip ? TWISTING_VINES : TWISTING_VINES_PLANT; + default -> null; + }; + } + + private static PlatformBlockState stackedVineBlock(PlatformBlockState state, int stack, int index) { + String key = stackedVineKey(state, stack, index); + if (key == null) { + return state; + } + return switch (key) { + case WEEPING_VINES -> weepingVines == null ? weepingVines = B.getState(key) : weepingVines; + case WEEPING_VINES_PLANT -> weepingVinesPlant == null ? weepingVinesPlant = B.getState(key) : weepingVinesPlant; + case TWISTING_VINES -> twistingVines == null ? twistingVines = B.getState(key) : twistingVines; + case TWISTING_VINES_PLANT -> twistingVinesPlant == null ? twistingVinesPlant = B.getState(key) : twistingVinesPlant; + default -> state; + }; + } } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java index 854fd00b2..d49f384e1 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java @@ -65,7 +65,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator { return; } - if (!DecoratorCore.isValidShorelineSupport(decorator, data.get(x, height, z))) { + PlatformBlockState support = data.get(x, height, z); + if (support == null || !support.isSolid()) { return; } @@ -76,7 +77,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator { return; } PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); - if (block != null) { + if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) { data.set(x, targetY, z, block); } return; @@ -96,7 +97,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator { } PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()); - if (block != null) { + if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)) { data.set(x, targetY, z, block); } return; @@ -116,6 +117,9 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator { if (block == null) { break; } + if (i == 0 && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) { + break; + } data.set(x, targetY, z, block); } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java index e3a9372d6..292495ac4 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineAssignedWorldManager.java @@ -26,10 +26,6 @@ import art.arcane.iris.core.events.IrisEngineHotloadEvent; 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.hud.HudPriority; -import art.arcane.volmlib.util.hud.HudSlotClaim; -import art.arcane.volmlib.util.hud.HudSlotRequest; -import art.arcane.volmlib.util.hud.HudSurface; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -40,7 +36,6 @@ import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.world.WorldSaveEvent; -import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -98,12 +93,8 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent runManagerTask("bukkit_world_manager_hotload_event", () -> { for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) { i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f); - HudSlotClaim claim = BukkitPlatform.hudSlots().open(i, new HudSlotRequest("iris:hotload", HudPriority.NOTICE, 2000L, List.of(HudSurface.TITLE))); - if (claim.resolve() != HudSurface.TITLE) { - continue; - } VolmitSender s = new VolmitSender(i); - s.sendTitle(C.IRIS + "" + IrisLanguage.text(RuntimeUiMessages.ENGINE_HOTLOADED), 70, 60, 410); + s.sendAction(C.IRIS + IrisLanguage.text(RuntimeUiMessages.ENGINE_HOTLOADED)); } }); } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java index 8f0290787..7e1febdd6 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStore.java @@ -169,7 +169,10 @@ public final class NativeStructureOwnershipStore { if (closed) { return; } - flushDirty(); + if (!flushDirty()) { + throw new IllegalStateException( + "Cannot close native structure ownership storage while Mantle regions are in use"); + } closed = true; authorities.invalidateAll(); } finally { @@ -177,12 +180,15 @@ public final class NativeStructureOwnershipStore { } } - private void flushDirty() { + private boolean flushDirty() { if (!dirty) { - return; + return true; } - storage.flush(); - dirty = false; + if (storage.flush()) { + dirty = false; + return true; + } + return false; } private Authority loadAuthority( @@ -217,7 +223,7 @@ public final class NativeStructureOwnershipStore { void remove(long target, String structureKey, int originChunkX, int originChunkZ); - void flush(); + boolean flush(); } private static final class MantleStorage implements Storage { @@ -296,10 +302,10 @@ public final class NativeStructureOwnershipStore { } @Override - public void flush() { + public boolean flush() { Map pending = new TreeMap<>(pendingBundles); if (pending.isEmpty()) { - return; + return true; } Mantle mantle = mantle(); @@ -309,10 +315,15 @@ public final class NativeStructureOwnershipStore { restorePendingBundle(mantle, target, entry.getValue()); regions.add(Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5)); } - mantle.saveTectonicPlates(regions); + Set deferredRegions = mantle.saveIdleTectonicPlates(regions); for (Map.Entry entry : pending.entrySet()) { - pendingBundles.remove(entry.getKey(), entry.getValue()); + long target = entry.getKey(); + long region = Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5); + if (!deferredRegions.contains(region)) { + pendingBundles.remove(target, entry.getValue()); + } } + return deferredRegions.isEmpty(); } private void restorePendingBundle(Mantle mantle, long target, 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 7c9f0e7b0..949fea43b 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 @@ -412,7 +412,7 @@ public class MantleObjectComponent extends IrisMantleComponent { CaveAnchorCache caveAnchorCache = new CaveAnchorCache(); for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) { boolean treePlacement = p instanceof IrisProceduralTree; - boolean chancePassed = rng.chance(p.getChance() + rng.d(-0.005, 0.005)); + boolean chancePassed = passesProceduralChance(rng, p.getChance()); if (golden) { IrisLogging.info("Goldendebug procedural chance: chunk=" + x + "," + z + " scope=" + scope @@ -541,6 +541,16 @@ public class MantleObjectComponent extends IrisMantleComponent { } } + static boolean passesProceduralChance(RNG rng, double chance) { + if (chance <= 0.0) { + return false; + } + if (chance >= 1.0) { + return true; + } + return rng.chance(Math.max(0.0, Math.min(1.0, chance + rng.d(-0.005, 0.005)))); + } + private CavePlacementAnchor findCavePlacementAnchor( MantleWriter writer, RNG rng, 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 60d4345a0..460042988 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 @@ -27,6 +27,7 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.engine.object.IrisProceduralBlocks; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.documentation.ChunkCoordinates; @@ -79,7 +80,6 @@ public class IrisCarveModifier extends EngineAssignedModifier customBiomeCache = scratch.customBiomeCache; UpperDimensionContext upperCtx = getEngine().getUpperContext(); @@ -155,7 +155,7 @@ public class IrisCarveModifier extends EngineAssignedModifier mc, CarveWallBuffer walls, CarveColumnMask[] boundaryMasks, - MatterCavern[] boundaryCaverns, int chunkX, int chunkZ, int[] surfaceHeights @@ -349,16 +344,16 @@ public class IrisCarveModifier extends EngineAssignedModifier neighborChunk, CarveWallBuffer walls, CarveColumnMask[] boundaryMasks, - MatterCavern[] boundaryCaverns, int localX, int yy, int localZ, @@ -388,9 +382,6 @@ public class IrisCarveModifier extends EngineAssignedModifier existingMantleChunk(Mantle mantle, int chunkX, int chunkZ) { @@ -410,7 +401,8 @@ public class IrisCarveModifier extends EngineAssignedModifier caveBiomeCache + Long2ObjectOpenHashMap caveBiomeCache, + Map customBiomeCache ) { if (columnMask == null || columnMask.isEmpty()) { return; @@ -437,7 +429,7 @@ public class IrisCarveModifier extends EngineAssignedModifier output, CarveColumnMask boundaryMask, - MatterCavern cavern, + CarveWallBuffer walls, int columnIndex, int chunkX, int chunkZ, @@ -481,19 +473,19 @@ public class IrisCarveModifier extends EngineAssignedModifier output, - MatterCavern cavern, + CarveWallBuffer walls, int rx, int rz, int worldX, @@ -504,62 +496,60 @@ public class IrisCarveModifier extends EngineAssignedModifier caveBiomeCache, Map customBiomeCache ) { - int center = (zoneFloor + zoneCeiling) / 2; - String customBiome = cavern.getCustomBiome(); - IrisBiome biome = customBiome.isEmpty() - ? resolveCaveBiome(caveBiomeCache, worldX, center, worldZ, resolverState) - : resolveCustomBiome(customBiomeCache, customBiome); - - if (biome == null) { + IrisBiome floorBiome = resolveCaveBoundaryBiome( + walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ, + resolverState, caveBiomeCache, customBiomeCache); + IrisBiome ceilingBiome = resolveCaveBoundaryBiome( + walls.get(rx, zoneCeiling, rz), worldX, zoneCeiling, worldZ, + resolverState, caveBiomeCache, customBiomeCache); + if (floorBiome == null && ceilingBiome == null) { return; } - - KList floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex()); - for (int i = 0; i < zoneFloor - 1; i++) { - if (!floorLayers.hasIndex(i)) { - break; + if (floorBiome != null) { + KList floorLayers = floorBiome.generateLayers( + getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex()); + for (int i = 0; i < zoneFloor - 1; i++) { + if (!floorLayers.hasIndex(i)) { + break; + } + int floorY = zoneFloor - i - 1; + if (floorY < 0) { + break; + } + PlatformBlockState existing = output.getRaw(rx, floorY, rz); + PlatformBlockState layer = floorLayers.get(i); + if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) { + continue; + } + if (B.isOre(existing)) { + output.setRaw(rx, floorY, rz, B.toDeepSlateOre(existing, layer)); + continue; + } + output.setRaw(rx, floorY, rz, layer); } - - int fy = zoneFloor - i - 1; - if (fy < 0) { - break; - } - - PlatformBlockState down = output.getRaw(rx, fy, rz); - if (!B.isSolid(down)) { - break; - } - - PlatformBlockState layer = floorLayers.get(i); - if (B.isOre(down)) { - output.setRaw(rx, fy, rz, B.toDeepSlateOre(down, layer)); - continue; - } - - output.setRaw(rx, fy, rz, layer); } - int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); - KList ceilingLayers = biome.generateCeilingLayers(getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex()); - for (int i = 0; i < ceilingLayers.size(); i++) { - int cy = zoneCeiling + i + 1; - if (cy >= worldMaxY) { - break; + if (ceilingBiome != null) { + int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); + KList ceilingLayers = ceilingBiome.generateCeilingLayers( + getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex()); + for (int i = 0; i < ceilingLayers.size(); i++) { + int ceilingY = zoneCeiling + i + 1; + if (ceilingY >= worldMaxY) { + break; + } + PlatformBlockState existing = output.getRaw(rx, ceilingY, rz); + if (!B.isSolid(existing)) { + continue; + } + PlatformBlockState layer = ceilingLayers.get(i); + if (B.isOre(existing)) { + output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer)); + continue; + } + output.setRaw(rx, ceilingY, rz, layer); } - - PlatformBlockState up = output.getRaw(rx, cy, rz); - if (!B.isSolid(up)) { - continue; - } - - PlatformBlockState layer = ceilingLayers.get(i); - if (B.isOre(up)) { - output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, layer)); - continue; - } - - output.setRaw(rx, cy, rz, layer); } } @@ -575,10 +565,8 @@ public class IrisCarveModifier extends EngineAssignedModifier output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { - int center = (zone.floor + zone.ceiling) / 2; + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { int maxY = output.getHeight(); - String customBiome = ""; if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) { output.setRaw(rx, zone.ceiling + 1, rz, AIR); @@ -599,82 +587,108 @@ public class IrisCarveModifier extends EngineAssignedModifier blocks = biome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex()); - - for (int i = 0; i < zone.floor - 1; i++) { - if (!blocks.hasIndex(i)) { - break; + if (floorBiome != null) { + KList floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex()); + for (int i = 0; i < zone.floor - 1; i++) { + if (!floorBlocks.hasIndex(i)) { + break; + } + int y = zone.floor - i - 1; + PlatformBlockState block = floorBlocks.get(i); + PlatformBlockState existing = output.getRaw(rx, y, rz); + if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) { + continue; + } + if (B.isOre(existing)) { + output.setRaw(rx, y, rz, B.toDeepSlateOre(existing, block)); + continue; + } + output.setRaw(rx, y, rz, block); } - int y = zone.floor - i - 1; - - PlatformBlockState b = blocks.get(i); - PlatformBlockState down = output.getRaw(rx, y, rz); - - if (!B.isSolid(down)) { - continue; - } - - if (B.isOre(down)) { - output.setRaw(rx, y, rz, B.toDeepSlateOre(down, b)); - continue; - } - - output.setRaw(rx, y, rz, blocks.get(i)); } - blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); - - for (int i = 0; i < blocks.size(); i++) { - int cy = zone.ceiling + i + 1; - if (cy >= maxY) { - break; + if (ceilingBiome != null) { + KList ceilingBlocks = ceilingBiome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); + for (int i = 0; i < ceilingBlocks.size(); i++) { + int cy = zone.ceiling + i + 1; + if (cy >= maxY) { + break; + } + PlatformBlockState block = ceilingBlocks.get(i); + PlatformBlockState existing = output.getRaw(rx, cy, rz); + if (!B.isSolid(existing)) { + continue; + } + if (B.isOre(existing)) { + output.setRaw(rx, cy, rz, B.toDeepSlateOre(existing, block)); + continue; + } + output.setRaw(rx, cy, rz, block); } - - PlatformBlockState b = blocks.get(i); - PlatformBlockState up = output.getRaw(rx, cy, rz); - - if (!B.isSolid(up)) { - continue; - } - - if (B.isOre(up)) { - output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, b)); - continue; - } - - output.setRaw(rx, cy, rz, b); } - IrisDecorator[] surfaceDecorators = biome.getDecoratorBucket(IrisDecorationPart.NONE); - if (surfaceDecorators.length > 0 && zone.getFloor() > 0 && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) { - decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness()); + IrisDecorator[] surfaceDecorators = floorBiome == null + ? new IrisDecorator[0] + : floorBiome.getDecoratorBucket(IrisDecorationPart.NONE); + if (surfaceDecorators.length > 0 && hasStableCaveFloorSupport(output, rx, zone.getFloor(), rz)) { + decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, floorBiome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness()); } - IrisDecorator[] ceilingDecorators = biome.getDecoratorBucket(IrisDecorationPart.CEILING); + IrisDecorator[] ceilingDecorators = ceilingBiome == null + ? new IrisDecorator[0] + : ceilingBiome.getDecoratorBucket(IrisDecorationPart.CEILING); if (ceilingDecorators.length > 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) { - decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getCeiling(), zone.airThickness()); + decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness()); } } + IrisBiome resolveCaveBoundaryBiome(MantleChunk mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + MatterCavern cavern = mantleChunk.get(x, y, z, MatterCavern.class); + return resolveCaveBoundaryBiome( + cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache); + } + + IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { + if (cavern != null && !cavern.getCustomBiome().isEmpty()) { + return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome()); + } + return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState); + } + + static boolean canReplaceCaveFloorLayer(Hunk output, int x, int y, int z, PlatformBlockState layer) { + return !isGravityAffected(layer) || y > 0 && B.isSolid(output.getRaw(x, y - 1, z)); + } + + static boolean hasStableCaveFloorSupport(Hunk output, int x, int floorY, int z) { + if (floorY <= 0) { + return false; + } + PlatformBlockState support = output.getRaw(x, floorY - 1, z); + if (!B.isSolid(support)) { + return false; + } + return !isGravityAffected(support) || floorY > 1 && B.isSolid(output.getRaw(x, floorY - 2, z)); + } + + static boolean isGravityAffected(PlatformBlockState state) { + if (state == null) { + return false; + } + String key = IrisProceduralBlocks.materialKey(state); + return key.equals("minecraft:sand") + || key.equals("minecraft:red_sand") + || key.equals("minecraft:gravel") + || key.equals("minecraft:suspicious_sand") + || key.equals("minecraft:suspicious_gravel") + || key.endsWith("_concrete_powder"); + } + private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) { IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState); if (center == null) { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java index 9b4470ca9..678fbd38c 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java @@ -29,7 +29,6 @@ import java.util.Map; final class IrisCarveScratch { final CarveColumnMask[] columnMasks = new CarveColumnMask[256]; final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256]; - final MatterCavern[] boundaryCaverns = new MatterCavern[256]; final int[] surfaceHeights = new int[256]; final CarveWallBuffer walls = new CarveWallBuffer(512); final Map customBiomeCache = new HashMap<>(); @@ -54,7 +53,6 @@ final class IrisCarveScratch { for (int index = 0; index < columnMasks.length; index++) { columnMasks[index].clear(); boundaryMasks[index].clear(); - boundaryCaverns[index] = null; } walls.clear(); customBiomeCache.clear(); @@ -185,6 +183,21 @@ final class CarveWallBuffer { } } + MatterCavern get(int x, int y, int z) { + int key = pack(x, y, z); + int index = mix(key) & mask; + while (true) { + int existingKey = keys[index]; + if (existingKey == EMPTY_KEY) { + return null; + } + if (existingKey == key) { + return values[index]; + } + index = (index + 1) & mask; + } + } + void forEach(Consumer consumer) { for (int index = 0; index < keys.length; index++) { int key = keys[index]; diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java index 1de113c16..936a7569c 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisDepositModifier.java @@ -22,6 +22,8 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedModifier; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDepositGenerator; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; import art.arcane.iris.engine.object.IrisDepositVariant; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; @@ -114,26 +116,38 @@ public class IrisDepositModifier extends EngineAssignedModifier height - 2) continue; - int minY = Math.max(0, k.getMinHeight()); - int maxY = Math.min(height, Math.min(getEngine().getHeight() - 1, k.getMaxHeight())); - - if (minY >= maxY) + int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y)); + boolean oreDeposit = k.isOre(getData()); + IrisBiome surfaceBiome = context.getBiome().get(x, z); + IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter() + ? getEngine().getCaveBiome( + (cx << 4) + x, biomeY, (cz << 4) + z, carvingState) + : null; + if (!k.matchesBiome(surfaceBiome, depositBiome)) { continue; + } - int y = rng.i(minY, maxY + 1); - - if (y > k.getMaxHeight() || y < k.getMinHeight() || y > height - 2) - continue; - - if (k.isOre(getData())) { - IrisBiome depositBiome = getEngine().getCaveBiome( - (cx << 4) + x, y, (cz << 4) + z, carvingState); - + if (oreDeposit) { if (depositBiome != null) { double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier(); if (frequencyMultiplier < 1D @@ -146,7 +160,9 @@ public class IrisDepositModifier extends EngineAssignedModifier 15 || nx < 0 || ny >= getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) { continue; } - int columnSurfaceLimit = getDepositSurfaceLimit(cx, cz, nx, nz, he, context); - if (ny > columnSurfaceLimit) { + int columnSurface = getDepositTerrainSurface(cx, cz, nx, nz, he, context); + if (!placementSurfaceAllows( + k.getPlacementScope(), ny, columnSurface, k.getSurfaceClearance())) { continue; } @@ -172,9 +189,16 @@ public class IrisDepositModifier extends EngineAssignedModifier candidateY > surfaceY + clearance; + case FULL_HEIGHT -> true; + case TERRAIN -> candidateY <= surfaceY - clearance; + }; } static int absoluteWorldY(int minHeight, int localY) { @@ -208,6 +247,54 @@ public class IrisDepositModifier extends EngineAssignedModifier maximum) { + return Integer.MIN_VALUE; + } + if (minimum == maximum) { + return minimum; + } + if (distribution != IrisDepositHeightDistribution.TRIANGLE) { + return minimum + rng.nextInt(maximum - minimum + 1); + } + + int span = maximum - minimum; + int lowerHalf = span / 2; + int upperHalf = span - lowerHalf; + return minimum + rng.nextInt(upperHalf + 1) + rng.nextInt(lowerHalf + 1); + } + + static boolean shouldDiscardExposed(double chance, double sample, boolean adjacentToAir) { + return adjacentToAir && chance > 0D && sample < Math.min(1D, chance); + } + + static boolean isAdjacentToAir(Hunk data, int x, int y, int z) { + return isAirAt(data, x - 1, y, z) + || isAirAt(data, x + 1, y, z) + || isAirAt(data, x, y - 1, z) + || isAirAt(data, x, y + 1, z) + || isAirAt(data, x, y, z - 1) + || isAirAt(data, x, y, z + 1); + } + + private static boolean isAirAt(Hunk data, int x, int y, int z) { + if (x < 0 || x >= data.getWidth() + || y < 0 || y >= data.getHeight() + || z < 0 || z >= data.getDepth()) { + return false; + } + PlatformBlockState state = data.getRaw(x, y, z); + return state == null || state.isAir(); + } + static boolean passesOreFrequency(double multiplier, double sample) { return multiplier >= 1D || sample < Math.max(0D, multiplier); } 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 12dbdccfc..ec7001c39 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 @@ -178,8 +178,8 @@ public class IrisBiome extends IrisRegistrant implements IRare { @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") private KList layers = new KList().qadd(new IrisBiomePaletteLayer()); @ArrayType(type = IrisBiomePaletteLayer.class) - @Desc("Layers of materials placed on cave ceilings in this biome, indexed upward from the ceiling surface. Must not have more entries than layers, whose height generators it reuses.") - private KList caveCeilingLayers = new KList().qadd(new IrisBiomePaletteLayer()); + @Desc("Layers of materials placed on cave ceilings in this biome, indexed upward from the ceiling surface. Must not have more entries than layers, whose height generators it reuses. Omitting this leaves cave ceilings unchanged.") + private KList caveCeilingLayers = new KList<>(); @ArrayType(type = IrisBiomePaletteLayer.class) @Desc("Layers of materials filling the water column of sea biomes, indexed downward from the water surface. Anything below the last layer is filled with the dimension fluid palette, not stone.") private KList seaLayers = new KList<>(); 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 2e953697c..8db494a8b 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 @@ -119,7 +119,7 @@ public class IrisDecorator { return getHeightGenerator(rng, data) .fit(stackMin, stackMax, x / heightVariance.getZoom(), - z / heightVariance.getZoom()) + 1; + z / heightVariance.getZoom()); } public CNG getHeightGenerator(RNG rng, IrisData data) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java new file mode 100644 index 000000000..441fa833b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositBiomeScope.java @@ -0,0 +1,6 @@ +package art.arcane.iris.engine.object; + +public enum IrisDepositBiomeScope { + SURFACE, + CAVE +} 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 0942302f4..cc9f49615 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 @@ -28,6 +28,7 @@ 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.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.volmlib.util.collection.KList; @@ -51,27 +52,38 @@ public class IrisDepositGenerator { private final transient ConcurrentMap> objects = new ConcurrentHashMap<>(); private final transient AtomicCache> blockData = new AtomicCache<>(); private final transient AtomicCache ore = new AtomicCache<>(); + private final transient AtomicCache> replaceableBlockData = new AtomicCache<>(); private final transient ConcurrentMap> scaledObjects = new ConcurrentHashMap<>(); @Required - @MinNumber(0) + @MinNumber(-8192) @MaxNumber(8192) - @Desc("The minimum height this deposit can generate at, in engine-local Y where 0 is the bottom of the dimension, not world Y.") + @Desc("The inclusive minimum origin height in engine-local Y, where 0 is the bottom of the dimension. Negative values let an unclipped vanilla distribution taper into the world floor.") private int minHeight = 1; @Required - @MinNumber(0) + @MinNumber(-8192) @MaxNumber(8192) - @Desc("The maximum height this deposit can generate at, in engine-local Y where 0 is the bottom of the dimension, not world Y. Clump centers are additionally clamped to stay below the terrain surface.") + @Desc("The inclusive maximum origin height in engine-local Y, where 0 is the bottom of the dimension.") private int maxHeight = 75; + @Desc("How origin heights are sampled. CLIPPED_UNIFORM preserves Iris behavior by clipping the band to terrain; UNIFORM and TRIANGLE sample the authored band first and discard cells outside terrain or build height.") + private IrisDepositHeightDistribution heightDistribution = IrisDepositHeightDistribution.CLIPPED_UNIFORM; + @Desc("TERRAIN places below the generated surface. ABOVE_TERRAIN places only in existing solid hosts above it. FULL_HEIGHT permits existing solid hosts anywhere in the dimension.") + private IrisDepositPlacementScope placementScope = IrisDepositPlacementScope.TERRAIN; + @MinNumber(0) + @MaxNumber(256) + @Desc("Solid blocks kept between a deposit and the terrain surface. Vanilla-like deposits use 0; the Iris default remains 7.") + private int surfaceClearance = 7; @Required @MinNumber(0) @MaxNumber(8192) - @Desc("The minimum amount of deposit blocks per clump") + @Desc("The minimum Iris block count or vanilla configured vein size, selected according to shape.") private int minSize = 0; @Required @MinNumber(0) @MaxNumber(8192) - @Desc("The maximum amount of deposit blocks per clump") + @Desc("The maximum Iris block count or vanilla configured vein size, selected according to shape.") private int maxSize = 128; + @Desc("IRIS places a fixed-count cube clump. VANILLA_ELLIPSOID uses Minecraft's chained ellipsoid geometry. VANILLA_SCATTERED uses Minecraft's sparse candidate offsets.") + private IrisDepositShape shape = IrisDepositShape.IRIS; @Required @MinNumber(0) @MaxNumber(2048) @@ -90,6 +102,10 @@ public class IrisDepositGenerator { @MaxNumber(1) @Desc("The chance of each individual clump spawning in a chunk") private double perClumpSpawnChance = 1; + @MinNumber(0) + @MaxNumber(1) + @Desc("Chance to discard a candidate ore block when it touches air. This matches vanilla ore exposure reduction.") + private double discardChanceOnAirExposure = 0; @Required @ArrayType(min = 1, type = IrisBlockData.class) @Desc("The palette of blocks to be used in this deposit generator. Each entry is picked uniformly; the per-entry weight field is ignored here.") @@ -98,10 +114,25 @@ public class IrisDepositGenerator { @MaxNumber(64) @Desc("Ore varience is how many different objects clumps iris will create") private int varience = 3; + @ArrayType(min = 1, type = String.class) + @Desc("Optional block ids this deposit may replace. An empty list retains Iris's any-solid behavior.") + private KList replaceableBlocks = new KList<>(); + @Desc("Chooses whether includedBiomes and excludedBiomes inspect the surface biome or the cave biome at the deposit origin.") + private IrisDepositBiomeScope biomeScope = IrisDepositBiomeScope.CAVE; + @ArrayType(min = 1, type = String.class) + @Desc("Optional Iris biome keys or vanilla derivative ids allowed for this deposit.") + private KList includedBiomes = new KList<>(); + @ArrayType(min = 1, type = String.class) + @Desc("Optional Iris biome keys or vanilla derivative ids denied for this deposit.") + private KList excludedBiomes = new KList<>(); @Desc("If set to true, this deposit will replace bedrock") private boolean replaceBedrock = false; public IrisObject getClump(Engine engine, RNG rng, IrisData rdata) { + if (shape != IrisDepositShape.IRIS) { + return generateConfiguredClumpObject(rng, rdata, minSize, maxSize); + } + ClumpCacheKey cacheKey = new ClumpCacheKey(engine.getSeedManager().getDeposit(), minSize, maxSize); KList objects = this.objects.computeIfAbsent(cacheKey, key -> { RNG rngv = new RNG(key.depositSeed() + hashCode()); @@ -124,6 +155,10 @@ public class IrisDepositGenerator { int scaledMinSize = scaledDepositSize(minSize, sizeMultiplier); int scaledMaxSize = scaledDepositSize(maxSize, sizeMultiplier); + if (shape != IrisDepositShape.IRIS) { + return generateConfiguredClumpObject(rng, rdata, scaledMinSize, scaledMaxSize); + } + ClumpCacheKey cacheKey = new ClumpCacheKey( engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize); KList objects = scaledObjects.computeIfAbsent(cacheKey, key -> { @@ -149,6 +184,148 @@ public class IrisDepositGenerator { return Math.max(0, Math.min(8192, (int) Math.round(size * multiplier))); } + private IrisObject generateConfiguredClumpObject(RNG rng, IrisData rdata, int clumpMinSize, int clumpMaxSize) { + int size = rng.i(clumpMinSize, clumpMaxSize + 1); + return switch (shape) { + case VANILLA_ELLIPSOID -> generateVanillaEllipsoid(rng, rdata, size); + case VANILLA_SCATTERED -> generateVanillaScattered(rng, rdata, size); + case IRIS -> generateClumpObject(rng, rdata, clumpMinSize, clumpMaxSize); + }; + } + + IrisObject generateVanillaEllipsoid(RNG rng, IrisData rdata, int size) { + if (size <= 0) { + return new IrisObject(1, 1, 1); + } + + float angle = rng.nextFloat() * (float) Math.PI; + float reach = size / 8F; + double startX = Math.sin(angle) * reach; + double endX = -Math.sin(angle) * reach; + double startZ = Math.cos(angle) * reach; + double endZ = -Math.cos(angle) * reach; + double startY = rng.nextInt(3) - 2; + double endY = rng.nextInt(3) - 2; + double[] nodes = new double[size * 4]; + + for (int i = 0; i < size; i++) { + float progress = (float) i / size; + double radiusNoise = rng.nextDouble() * size / 16D; + nodes[i * 4] = startX + (endX - startX) * progress; + nodes[i * 4 + 1] = startY + (endY - startY) * progress; + nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress; + nodes[i * 4 + 3] = ((Math.sin(Math.PI * progress) + 1D) * radiusNoise + 1D) / 2D; + } + + for (int i = 0; i < size - 1; i++) { + if (nodes[i * 4 + 3] <= 0D) { + continue; + } + for (int j = i + 1; j < size; j++) { + if (nodes[j * 4 + 3] <= 0D) { + continue; + } + double dx = nodes[i * 4] - nodes[j * 4]; + double dy = nodes[i * 4 + 1] - nodes[j * 4 + 1]; + double dz = nodes[i * 4 + 2] - nodes[j * 4 + 2]; + double dr = nodes[i * 4 + 3] - nodes[j * 4 + 3]; + if (dr * dr <= dx * dx + dy * dy + dz * dz) { + continue; + } + if (dr > 0D) { + nodes[j * 4 + 3] = -1D; + } else { + nodes[i * 4 + 3] = -1D; + } + } + } + + KSet cells = new KSet<>(); + for (int i = 0; i < size; i++) { + double radius = nodes[i * 4 + 3]; + if (radius < 0D) { + continue; + } + double centerX = nodes[i * 4]; + double centerY = nodes[i * 4 + 1]; + double centerZ = nodes[i * 4 + 2]; + int minX = (int) Math.floor(centerX - radius); + int maxX = Math.max((int) Math.floor(centerX + radius), minX); + int minY = (int) Math.floor(centerY - radius); + int maxY = Math.max((int) Math.floor(centerY + radius), minY); + int minZ = (int) Math.floor(centerZ - radius); + int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ); + + for (int x = minX; x <= maxX; x++) { + double nx = (x + 0.5D - centerX) / radius; + if (nx * nx >= 1D) { + continue; + } + for (int y = minY; y <= maxY; y++) { + double ny = (y + 0.5D - centerY) / radius; + if (nx * nx + ny * ny >= 1D) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + double nz = (z + 0.5D - centerZ) / radius; + if (nx * nx + ny * ny + nz * nz < 1D) { + cells.add(new BlockPosition(x, y, z)); + } + } + } + } + } + + return objectFromCells(cells, rng, rdata); + } + + IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) { + KSet cells = new KSet<>(); + int candidates = rng.nextInt(Math.max(0, size) + 1); + for (int i = 0; i < candidates; i++) { + int magnitude = Math.min(i, 7); + cells.add(new BlockPosition( + Math.round((rng.nextFloat() - rng.nextFloat()) * magnitude), + Math.round((rng.nextFloat() - rng.nextFloat()) * magnitude), + Math.round((rng.nextFloat() - rng.nextFloat()) * magnitude))); + } + return objectFromCells(cells, rng, rdata); + } + + private IrisObject objectFromCells(KSet cells, RNG rng, IrisData rdata) { + if (cells.isEmpty()) { + return new IrisObject(1, 1, 1); + } + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (BlockPosition cell : cells) { + minX = Math.min(minX, cell.getX()); + minY = Math.min(minY, cell.getY()); + minZ = Math.min(minZ, cell.getZ()); + maxX = Math.max(maxX, cell.getX()); + maxY = Math.max(maxY, cell.getY()); + maxZ = Math.max(maxZ, cell.getZ()); + } + + int extentX = Math.max(Math.abs(minX), Math.abs(maxX)); + int extentY = Math.max(Math.abs(minY), Math.abs(maxY)); + int extentZ = Math.max(Math.abs(minZ), Math.abs(maxZ)); + IrisObject object = new IrisObject(extentX * 2 + 1, extentY * 2 + 1, extentZ * 2 + 1); + for (BlockPosition cell : cells) { + object.setUnsigned( + cell.getX() + extentX, + cell.getY() + extentY, + cell.getZ() + extentZ, + nextBlock(rng, rdata)); + } + return object; + } + private IrisObject generateClumpObject(RNG rngv, IrisData rdata, int clumpMinSize, int clumpMaxSize) { int s = rngv.i(clumpMinSize, clumpMaxSize + 1); if (s == 1) { @@ -230,6 +407,54 @@ public class IrisDepositGenerator { }); } + public boolean canReplace(PlatformBlockState state) { + if (replaceableBlocks == null || replaceableBlocks.isEmpty()) { + return true; + } + return replaceableBlockData.aquire(this::resolveReplaceableBlocks) + .contains(IrisProceduralBlocks.materialKey(state)); + } + + public boolean matchesBiome(IrisBiome surfaceBiome, IrisBiome caveBiome) { + IrisBiome selected = biomeScope == IrisDepositBiomeScope.SURFACE ? surfaceBiome : caveBiome; + if (includedBiomes != null && !includedBiomes.isEmpty() && !matchesAnyBiome(selected, includedBiomes)) { + return false; + } + return excludedBiomes == null || excludedBiomes.isEmpty() || !matchesAnyBiome(selected, excludedBiomes); + } + + public boolean usesCaveBiomeFilter() { + return biomeScope == IrisDepositBiomeScope.CAVE + && ((includedBiomes != null && !includedBiomes.isEmpty()) + || (excludedBiomes != null && !excludedBiomes.isEmpty())); + } + + private KSet resolveReplaceableBlocks() { + KSet resolved = new KSet<>(); + for (String key : replaceableBlocks) { + PlatformBlockState state = B.getStateOrNull(key, false); + if (state != null) { + resolved.add(IrisProceduralBlocks.materialKey(state)); + } + } + return resolved; + } + + private boolean matchesAnyBiome(IrisBiome biome, KList configuredBiomes) { + if (biome == null) { + return false; + } + for (String configured : configuredBiomes) { + String namespaced = configured.contains(":") ? configured : "minecraft:" + configured; + if (configured.equals(biome.getLoadKey()) + || namespaced.equals(biome.getDerivativeKey()) + || namespaced.equals(biome.getVanillaDerivativeKey())) { + return true; + } + } + return false; + } + record ClumpCacheKey(long depositSeed, int minSize, int maxSize) { } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java new file mode 100644 index 000000000..75bfd3f4b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistribution.java @@ -0,0 +1,7 @@ +package art.arcane.iris.engine.object; + +public enum IrisDepositHeightDistribution { + CLIPPED_UNIFORM, + UNIFORM, + TRIANGLE +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java new file mode 100644 index 000000000..756ef0b9c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java @@ -0,0 +1,7 @@ +package art.arcane.iris.engine.object; + +public enum IrisDepositPlacementScope { + TERRAIN, + ABOVE_TERRAIN, + FULL_HEIGHT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java new file mode 100644 index 000000000..e6f6e82b0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java @@ -0,0 +1,7 @@ +package art.arcane.iris.engine.object; + +public enum IrisDepositShape { + IRIS, + VANILLA_ELLIPSOID, + VANILLA_SCATTERED +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java index 6b662459a..18301b3d9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java @@ -38,7 +38,7 @@ import lombok.experimental.Accessors; @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor -@Desc("A single procedurally generated rock formation (a natural landmark such as a spire, hoodoo, arch, sea stack, boulder or basalt column cluster). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file.") +@Desc("A single procedurally generated natural formation, including rock landmarks and magical glacial silhouettes. Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file.") @Data public class IrisFormation implements IrisProceduralPlacement { private final transient AtomicCache> variantCache = new AtomicCache<>(); @@ -185,6 +185,11 @@ public class IrisFormation implements IrisProceduralPlacement { @Desc("The thickness in blocks of the ARCH legs and spanning curve.") private int archThickness = 3; + @MinNumber(0) + @MaxNumber(1) + @Desc("How strongly an ARCH varies its leg steepness, crown position, depth bow and tube width between deterministic variants. 0 is symmetric and 1 is highly organic.") + private double archAsymmetry = 0.35; + @MinNumber(2) @MaxNumber(12) @Desc("How many separate columns make up a BASALT_COLUMN cluster. Each column gets a randomized height around the formation height range.") @@ -199,6 +204,46 @@ public class IrisFormation implements IrisProceduralPlacement { @Desc("How much the heights of individual BASALT_COLUMN columns vary from each other, from 0 (all equal) to 1 (highly varied).") private double basaltHeightVariance = 0.45; + @MinNumber(1) + @MaxNumber(12) + @Desc("How many tapered summits crown an ICEBERG formation.") + private int icebergPeaks = 3; + + @MinNumber(2) + @MaxNumber(8) + @Desc("How many separated shards form a FISSURE.") + private int fractureCount = 3; + + @MinNumber(1) + @MaxNumber(16) + @Desc("Open-air separation in blocks between the shards of a FISSURE.") + private int fractureSeparation = 2; + + @MinNumber(0.25) + @MaxNumber(6) + @Desc("How many complete turns a SPIRAL makes from base to tip.") + private double spiralTurns = 1.5; + + @MinNumber(1) + @MaxNumber(32) + @Desc("The starting distance in blocks between a SPIRAL and its open center.") + private int spiralRadius = 4; + + @MinNumber(1) + @MaxNumber(8) + @Desc("The radius in blocks of the tube swept along a SPIRAL.") + private int spiralThickness = 2; + + @MinNumber(1) + @MaxNumber(32) + @Desc("How far the hooked arm of an OVERHANG projects from its base.") + private int overhangReach = 8; + + @MinNumber(0) + @MaxNumber(16) + @Desc("How many blocks the tip of an OVERHANG curls downward.") + private int overhangDrop = 3; + public KList getVariantObjects(IrisData data) { return variantCache.aquire(() -> { KList baked = new KList<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java index 24f83084a..ce0b6eae8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFormationForm.java @@ -20,7 +20,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.object.annotations.Desc; -@Desc("The overall silhouette of a procedural rock formation. Each form drives a distinct sculpting routine in the formation generator.") +@Desc("The overall silhouette of a procedural natural formation. Each form drives a distinct sculpting routine in the formation generator.") public enum IrisFormationForm { @Desc("A tall, slender needle of rock that tapers smoothly to a point at the top (a sharp pinnacle or stone spire).") SPIRE, @@ -33,5 +33,13 @@ public enum IrisFormationForm { @Desc("A rounded, lumpy boulder formed from a noise-perturbed ellipsoid that sits low on the terrain.") BOULDER, @Desc("A tightly packed cluster of several vertical, near-hexagonal columns of varying height (a basalt column formation / giant's causeway).") - BASALT_COLUMN + BASALT_COLUMN, + @Desc("A broad, faceted mound crowned by several irregular tapered peaks, suitable for icebergs and glacial outcrops.") + ICEBERG, + @Desc("Several separated, outward-leaning shards divided by open cracks.") + FISSURE, + @Desc("A tapered tube swept around a tightening helix, leaving open air through its curled center.") + SPIRAL, + @Desc("A grounded pillar that bends into a long hooked cantilever and curls downward at its tip.") + OVERHANG } 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 index 00cf0bc1c..1cdf98bc1 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java @@ -896,8 +896,9 @@ final class IrisObjectPlacementRunner { return false; } - int envelopeMinY = paint ? Integer.MIN_VALUE : y - margin; - int envelopeMaxY = paint ? Integer.MAX_VALUE : y + margin; + int worldY = y + engine.getMinHeight(); + int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin; + int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin; boolean envelopeMeetsPiece = false; for (NativeStructureVolume volume : volumes) { if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) { @@ -934,9 +935,10 @@ final class IrisObjectPlacementRunner { ? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()) : y + (int) Math.round(i.getY()); + int worldBlockY = yy + engine.getMinHeight(); for (NativeStructureVolume volume : volumes) { - if (volume.containsWithin(xx, yy, zz, warpMargin)) { + if (volume.containsWithin(xx, worldBlockY, zz, warpMargin)) { return true; } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java index 19d773660..e6fe4e577 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationGenerator.java @@ -51,6 +51,10 @@ public final class FormationGenerator { case SEA_STACK -> FormationShapeBuilder.seaStack(canvas, f, height, baseRadius, rng); case BOULDER -> FormationShapeBuilder.boulder(canvas, f, height, baseRadius, rng); case BASALT_COLUMN -> FormationShapeBuilder.basaltColumns(canvas, f, height, baseRadius, rng); + case ICEBERG -> FormationShapeBuilder.iceberg(canvas, f, height, baseRadius, rng); + case FISSURE -> FormationShapeBuilder.fissure(canvas, f, height, baseRadius, rng); + case SPIRAL -> FormationShapeBuilder.spiral(canvas, f, height, baseRadius, rng); + case OVERHANG -> FormationShapeBuilder.overhang(canvas, f, height, baseRadius, rng); } if (canvas.isEmpty()) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java index bc0936633..54f3026c0 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java +++ b/core/src/main/java/art/arcane/iris/engine/object/formation/FormationShapeBuilder.java @@ -20,8 +20,12 @@ package art.arcane.iris.engine.object.formation; import art.arcane.iris.engine.object.IrisFormation; import art.arcane.iris.engine.object.tree.TreeFunctions; +import art.arcane.iris.util.common.math.Vector3i; import art.arcane.volmlib.util.math.RNG; +import java.util.HashMap; +import java.util.Map; + public final class FormationShapeBuilder { private FormationShapeBuilder() { } @@ -29,6 +33,24 @@ public final class FormationShapeBuilder { public static void spire(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { double topRadius = f.getTopWidth(); column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, true); + double lean = Math.toRadians(f.getLean()); + double azimuth = Math.toRadians(f.getLeanAzimuth()); + int previousX = 0; + int previousZ = 0; + for (int y = 0; y < height; y++) { + double shear = Math.tan(lean) * y; + int centerX = (int) Math.round(Math.cos(azimuth) * shear); + int centerZ = (int) Math.round(Math.sin(azimuth) * shear); + boolean cap = y >= height - 2; + for (int x = Math.min(previousX, centerX); x <= Math.max(previousX, centerX); x++) { + setSpireCenter(canvas, x, y, previousZ, cap); + } + for (int z = Math.min(previousZ, centerZ); z <= Math.max(previousZ, centerZ); z++) { + setSpireCenter(canvas, centerX, y, z, cap); + } + previousX = centerX; + previousZ = centerZ; + } } public static void seaStack(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { @@ -88,44 +110,57 @@ public final class FormationShapeBuilder { } public static void arch(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { - int span = Math.max(2, f.getArchSpan()); - int thickness = Math.max(1, f.getArchThickness()); - int legHeight = Math.max(2, (int) Math.round(height * 0.55)); - int halfSpan = span / 2; - double legRadius = Math.max(1.0, thickness / 2.0 + baseRadius * 0.25); - long noiseSeed = f.getSeed() + 7777L; - double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + double span = Math.max(2.0, f.getArchSpan()); + double tubeRadius = Math.max(0.75, f.getArchThickness() * 0.5); + double asymmetry = Math.max(0.0, Math.min(1.0, f.getArchAsymmetry())); + double leftFootX = -span * 0.5 - tubeRadius - rng.d(0.0, span * 0.16) * asymmetry; + double rightFootX = span * 0.5 + tubeRadius + rng.d(0.0, span * 0.16) * asymmetry; + double leftFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry; + double rightFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry; + double crownZ = rng.d(-span * 0.2, span * 0.2) * asymmetry; + double crownShiftX = rng.d(-span * 0.12, span * 0.12) * asymmetry; + double depthTwist = rng.d(-span * 0.1, span * 0.1) * asymmetry; + double crownPosition = 0.5 + rng.d(-0.12, 0.12) * asymmetry; + double leftSteepness = 0.58 + rng.d(-0.16, 0.16) * asymmetry; + double rightSteepness = 0.58 + rng.d(-0.16, 0.16) * asymmetry; + double radiusWave = rng.d(-0.22, 0.22) * asymmetry; + double radiusPhase = rng.d(0.0, Math.PI * 2.0); + double crownCenterY = Math.max(tubeRadius + 2.0, height - 1.0 - tubeRadius); + double footRadius = tubeRadius + Math.min(tubeRadius * 0.45, baseRadius * 0.18); + long noiseSeed = rng.nextLong(); + int steps = Math.max(24, (int) Math.ceil((rightFootX - leftFootX + height) * 4.0)); - for (int side = -1; side <= 1; side += 2) { - int legX = side * (halfSpan + (int) Math.ceil(legRadius)); - for (int y = 0; y < legHeight; y++) { - disc(canvas, f, legX, y, 0, legRadius, false, rng); + ball(canvas, f, (int) Math.round(leftFootX), (int) Math.round(footRadius), + (int) Math.round(leftFootZ), footRadius, false, noiseSeed, rng); + ball(canvas, f, (int) Math.round(rightFootX), (int) Math.round(footRadius), + (int) Math.round(rightFootZ), footRadius, false, noiseSeed, rng); + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + double normalizedRise; + double steepness; + if (t <= crownPosition) { + normalizedRise = Math.sin((t / crownPosition) * Math.PI * 0.5); + steepness = leftSteepness; + } else { + normalizedRise = Math.sin(((1.0 - t) / (1.0 - crownPosition)) * Math.PI * 0.5); + steepness = rightSteepness; } + double rise = Math.pow(Math.max(0.0, normalizedRise), steepness); + double x = leftFootX + (rightFootX - leftFootX) * t + + Math.sin(Math.PI * t) * crownShiftX; + double inverse = 1.0 - t; + double z = inverse * inverse * leftFootZ + 2.0 * inverse * t * crownZ + t * t * rightFootZ + + Math.sin(Math.PI * 2.0 * t) * depthTwist; + double y = tubeRadius + (crownCenterY - tubeRadius) * rise; + double radius = tubeRadius * (1.0 + radiusWave * Math.sin(Math.PI * t) + * Math.sin(Math.PI * 2.0 * t + radiusPhase)); + radius = Math.max(tubeRadius * 0.68, Math.min(tubeRadius * 1.32, radius)); + ball(canvas, f, (int) Math.round(x), (int) Math.round(y), (int) Math.round(z), + radius, false, noiseSeed, rng); } - - int archTop = legHeight + (int) Math.round(span * 0.45); - int leftX = -(halfSpan + (int) Math.ceil(legRadius)); - int rightX = halfSpan + (int) Math.ceil(legRadius); - double archWidth = (rightX - leftX) / 2.0; - double centerX = (leftX + rightX) / 2.0; - double archHeight = archTop - legHeight; - int half = Math.max(1, thickness / 2); - - for (int x = leftX; x <= rightX; x++) { - double nx = (x - centerX) / archWidth; - if (nx < -1.0 || nx > 1.0) { - continue; - } - double curveY = legHeight + archHeight * Math.sqrt(Math.max(0.0, 1.0 - nx * nx)); - int yc = (int) Math.round(curveY); - for (int dy = -half; dy <= half; dy++) { - for (int z = -half; z <= half; z++) { - double wobble = (TreeFunctions.valueNoise3D(x, yc + dy, z, noiseSeed) - 0.5) * roughness; - if (z * z + dy * dy <= half * half + 0.5 + wobble) { - canvas.setBody(x, Math.max(0, yc + dy), z); - } - } - } + if (asymmetry == 0.0) { + mirrorAcrossX(canvas); } } @@ -159,6 +194,97 @@ public final class FormationShapeBuilder { } } + public static void iceberg(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + long noiseSeed = rng.nextLong(); + double bodyHeight = Math.max(2.0, height * 0.42); + double radiusX = baseRadius * rng.d(1.15, 1.5); + double radiusZ = baseRadius * rng.d(1.0, 1.35); + ellipsoid(canvas, radiusX, bodyHeight, radiusZ, bodyHeight * 0.36, noiseSeed, f.getRoughness()); + + int peaks = Math.max(1, f.getIcebergPeaks()); + double phase = rng.d(0.0, Math.PI * 2.0); + for (int i = 0; i < peaks; i++) { + double angle = phase + (Math.PI * 2.0 * i / peaks) + rng.d(-0.35, 0.35); + double distance = rng.d(0.0, baseRadius * 0.62); + int offsetX = (int) Math.round(Math.cos(angle) * distance); + int offsetZ = (int) Math.round(Math.sin(angle) * distance); + int peakHeight = Math.max(3, (int) Math.round(height * rng.d(0.58, 1.0))); + double peakRadius = Math.max(1.0, baseRadius * rng.d(0.28, 0.58)); + double leanDistance = rng.d(0.0, Math.max(1.0, baseRadius * 0.45)); + taperedPeak(canvas, f, peakHeight, peakRadius, offsetX, offsetZ, angle, leanDistance, rng); + } + } + + public static void fissure(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int count = Math.max(2, f.getFractureCount()); + double separation = Math.max(1.0, f.getFractureSeparation()); + double shardRadius = Math.max(1.0, baseRadius / Math.max(1.6, count * 0.72)); + double phase = rng.d(0.0, Math.PI * 2.0); + + for (int i = 0; i < count; i++) { + double lane = i - ((count - 1) / 2.0); + double offset = lane * (shardRadius * 2.0 + separation); + int offsetX = (int) Math.round(Math.cos(phase) * offset); + int offsetZ = (int) Math.round(Math.sin(phase) * offset); + double outwardAngle = lane < 0 ? phase + Math.PI : phase; + int shardHeight = Math.max(3, (int) Math.round(height * rng.d(0.62, 1.0))); + double leanDistance = rng.d(separation * 0.4, separation + baseRadius * 0.8); + taperedPeak(canvas, f, shardHeight, shardRadius, offsetX, offsetZ, outwardAngle, leanDistance, rng); + } + } + + public static void spiral(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int steps = Math.max(12, height * 4); + double turns = Math.max(0.25, f.getSpiralTurns()); + double startRadius = Math.max(1.0, f.getSpiralRadius()); + double startThickness = Math.max(1.0, f.getSpiralThickness()); + double phase = rng.d(0.0, Math.PI * 2.0); + long noiseSeed = rng.nextLong(); + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + double angle = phase + Math.PI * 2.0 * turns * t; + double radius = startRadius * (1.0 - 0.72 * t); + int x = (int) Math.round(Math.cos(angle) * radius); + int y = (int) Math.round(t * (height - 1)); + int z = (int) Math.round(Math.sin(angle) * radius); + double thickness = Math.max(0.75, startThickness * (1.0 - 0.48 * t)); + ball(canvas, f, x, y, z, thickness, step == steps, noiseSeed, rng); + } + + double footRadius = Math.max(1.0, Math.min(baseRadius, startThickness + 1.0)); + disc(canvas, f, (int) Math.round(Math.cos(phase) * startRadius), 0, + (int) Math.round(Math.sin(phase) * startRadius), footRadius, false, rng); + } + + public static void overhang(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) { + int steps = Math.max(12, height * 4); + double reach = Math.max(1.0, f.getOverhangReach()); + double drop = Math.max(0.0, f.getOverhangDrop()); + double phase = Math.toRadians(f.getLeanAzimuth()) + rng.d(-0.18, 0.18); + double trunkHeight = Math.max(3.0, height * 0.62); + long noiseSeed = rng.nextLong(); + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + double extension; + double y; + if (t <= 0.58) { + double rise = t / 0.58; + extension = reach * 0.16 * rise * rise; + y = trunkHeight * rise; + } else { + double hook = (t - 0.58) / 0.42; + extension = reach * (0.16 + 0.84 * Math.sin(hook * Math.PI * 0.5)); + y = trunkHeight + Math.sin(hook * Math.PI) * height * 0.18 - drop * hook * hook; + } + int x = (int) Math.round(Math.cos(phase) * extension); + int z = (int) Math.round(Math.sin(phase) * extension); + double thickness = Math.max(0.9, baseRadius * (1.0 - 0.64 * t)); + ball(canvas, f, x, Math.max(0, (int) Math.round(y)), z, thickness, step == steps, noiseSeed, rng); + } + } + private static void column(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, double topRadius, int extraBaseX, int extraBaseZ, RNG rng, boolean capTop) { double lean = Math.toRadians(f.getLean()); double azimuth = Math.toRadians(f.getLeanAzimuth()); @@ -177,6 +303,14 @@ public final class FormationShapeBuilder { } } + private static void setSpireCenter(FormationCanvas canvas, int x, int y, int z, boolean cap) { + if (cap) { + canvas.setCap(x, y, z); + } else { + canvas.setBody(x, y, z); + } + } + private static void ringDisc(FormationCanvas canvas, IrisFormation f, int cx, int y, int cz, double radius, double roughness, double jitter, long noiseSeed, boolean cap, RNG rng) { int r = (int) Math.ceil(radius) + 1; for (int x = -r; x <= r; x++) { @@ -212,4 +346,84 @@ public final class FormationShapeBuilder { } } } + + private static void taperedPeak(FormationCanvas canvas, IrisFormation f, int height, double radius, + int offsetX, int offsetZ, double leanAngle, double leanDistance, RNG rng) { + long noiseSeed = rng.nextLong(); + double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + double jitter = Math.max(0.0, Math.min(1.0, f.getJitter())); + for (int y = 0; y < height; y++) { + double t = height <= 1 ? 1.0 : y / (double) (height - 1); + int x = offsetX + (int) Math.round(Math.cos(leanAngle) * leanDistance * t * t); + int z = offsetZ + (int) Math.round(Math.sin(leanAngle) * leanDistance * t * t); + double layerRadius = Math.max(0.45, radius * Math.pow(1.0 - t, 0.72)); + ringDisc(canvas, f, x, y, z, layerRadius, roughness, jitter, noiseSeed, y >= height - 2, rng); + } + } + + private static void ellipsoid(FormationCanvas canvas, double radiusX, double radiusY, double radiusZ, + double centerY, long noiseSeed, double configuredRoughness) { + double roughness = Math.max(0.0, Math.min(1.0, configuredRoughness)); + int maxX = (int) Math.ceil(radiusX) + 1; + int maxY = (int) Math.ceil(radiusY) + 1; + int maxZ = (int) Math.ceil(radiusZ) + 1; + for (int x = -maxX; x <= maxX; x++) { + for (int y = 0; y <= maxY; y++) { + for (int z = -maxZ; z <= maxZ; z++) { + double nx = x / radiusX; + double ny = (y - centerY) / radiusY; + double nz = z / radiusZ; + double distance = nx * nx + ny * ny + nz * nz; + double wobble = (TreeFunctions.valueNoise3D(x, y, z, noiseSeed) - 0.5) * roughness * 0.55; + if (distance + wobble <= 1.0) { + canvas.setBody(x, y, z); + } + } + } + } + } + + private static void ball(FormationCanvas canvas, IrisFormation f, int centerX, int centerY, int centerZ, + double radius, boolean cap, long noiseSeed, RNG rng) { + int extent = (int) Math.ceil(radius); + double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); + double jitter = Math.max(0.0, Math.min(1.0, f.getJitter())); + for (int x = -extent; x <= extent; x++) { + for (int y = -extent; y <= extent; y++) { + for (int z = -extent; z <= extent; z++) { + double distance = Math.sqrt(x * x + y * y + z * z); + double perturb = (TreeFunctions.valueNoise3D(centerX + x, centerY + y, centerZ + z, noiseSeed) - 0.5) + * roughness * Math.max(1.0, radius * 0.6); + double effectiveRadius = radius + perturb; + if (distance > effectiveRadius + 0.2) { + continue; + } + if (jitter > 0.0 && distance > effectiveRadius - 0.75 && rng.chance(jitter * 0.35)) { + continue; + } + int targetY = centerY + y; + if (targetY < 0) { + continue; + } + if (cap) { + canvas.setCap(centerX + x, targetY, centerZ + z); + } else { + canvas.setBody(centerX + x, targetY, centerZ + z); + } + } + } + } + } + + private static void mirrorAcrossX(FormationCanvas canvas) { + Map cells = new HashMap<>(canvas.getCells()); + for (Map.Entry entry : cells.entrySet()) { + Vector3i position = entry.getKey(); + if (entry.getValue() == FormationCanvas.Role.CAP) { + canvas.setCap(-position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } else { + canvas.setBody(-position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + } + } } 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 5f801dc27..c48304286 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 @@ -33,8 +33,8 @@ import art.arcane.iris.util.common.misc.Bindings; import art.arcane.iris.util.common.plugin.VolmitPlugin; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.hud.HudActionBar; import art.arcane.volmlib.util.hud.HudBossBarLane; -import art.arcane.volmlib.util.hud.HudSlotService; import art.arcane.volmlib.util.math.Vector3d; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -65,7 +65,7 @@ import java.util.function.Supplier; public final class BukkitPlatform implements IrisPlatform { private static volatile Plugin PLUGIN; private static volatile Bindings.Adventure AUDIENCES; - private static volatile HudSlotService HUD_SLOTS; + private static volatile HudActionBar HUD_BAR; private static volatile HudBossBarLane HUD_LANES; private static volatile Supplier CONSOLE; private static volatile HostBridge BRIDGE; @@ -135,21 +135,21 @@ public final class BukkitPlatform implements IrisPlatform { return adventure; } - public static void hostHud(HudSlotService hudSlots, HudBossBarLane hudLanes) { - HUD_SLOTS = hudSlots; + public static void hostHud(HudActionBar hudBar, HudBossBarLane hudLanes) { + HUD_BAR = hudBar; HUD_LANES = hudLanes; } public static boolean hasHud() { - return HUD_SLOTS != null && HUD_LANES != null; + return HUD_BAR != null && HUD_LANES != null; } - public static HudSlotService hudSlots() { - HudSlotService hudSlots = HUD_SLOTS; - if (hudSlots == null) { - throw new IllegalStateException("No Iris HUD slot service is hosted"); + public static HudActionBar hudBar() { + HudActionBar hudBar = HUD_BAR; + if (hudBar == null) { + throw new IllegalStateException("No Iris HUD action bar is hosted"); } - return hudSlots; + return hudBar; } public static HudBossBarLane hudLanes() { 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 5de5b9165..fb46fed72 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 @@ -22,11 +22,12 @@ import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.IrisSettings; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.hud.HudSurface; +import art.arcane.volmlib.util.hud.HudPriority; +import art.arcane.volmlib.util.hud.HudSegment; +import art.arcane.volmlib.util.hud.HudSlot; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.math.M; -import art.arcane.iris.util.common.scheduling.J; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; @@ -47,9 +48,7 @@ import org.bukkit.plugin.Plugin; 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; /** * Represents a volume sender. A command sender with extra crap in it @@ -221,60 +220,37 @@ public class VolmitSender implements CommandSender { s.sendMessage("========================================================"); } - public void sendTitle(String title, String subtitle, int i, int s, int o) { - try { - player().sendTitle( - LegacyComponentSerializer.legacySection().serialize(createComponent(title)), - LegacyComponentSerializer.legacySection().serialize(createComponent(subtitle)), - i / 50, s / 50, o / 50); - } catch (Throwable ignored) { - } - } - - public void sendProgress(double percent, String thing, HudSurface titleSurface, HudSurface barSurface) { - if (percent < 0) { - int l = 44; - int g = (int) (1D * l); - if (titleSurface == HudSurface.TITLE) { - sendTitle(C.IRIS + thing + " ", 0, 500, 250); - } - if (barSurface == HudSurface.ACTION_BAR) { - sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + " " + Form.repeat(" ", g) + "" + Form.repeat(" ", l - g)); - } - } else { - int l = 44; - int g = (int) (percent * l); - if (titleSurface == HudSurface.TITLE) { - sendTitle(C.IRIS + thing + " " + C.BLUE + "" + Form.pc(percent, 0), 0, 500, 250); - } - if (barSurface == HudSurface.ACTION_BAR) { - sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + " " + Form.repeat(" ", g) + "" + Form.repeat(" ", l - g)); - } - } + public void sendProgress(double percent, String thing) { + int l = 44; + int g = (int) ((percent < 0 ? 1D : percent) * l); + sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + " " + Form.repeat(" ", g) + "" + Form.repeat(" ", l - g)); } public void sendAction(String action) { try { - player().spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(action)))); + deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(action))); } catch (Throwable ignored) { } } public void sendActionNoProcessing(String action) { try { - player().spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponentNoProcessing(action)))); + deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponentNoProcessing(action))); } catch (Throwable ignored) { } } - public void sendTitle(String subtitle, int i, int s, int o) { - try { - player().sendTitle( - " ", - LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(subtitle)), - i / 50, s / 50, o / 50); - } catch (Throwable ignored) { + private void deliverAction(String legacy) { + Player player = player(); + if (BukkitPlatform.hasHud()) { + if (legacy.isBlank()) { + BukkitPlatform.hudBar().clear(player, "iris:action"); + } else { + BukkitPlatform.hudBar().publish(player, new HudSegment("iris:action", HudPriority.PROGRESS, 3000L, java.util.List.of(HudSlot.CENTER, HudSlot.LEFT), legacy)); + } + return; } + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(legacy)); } private Component createNoPrefixComponent(String message) { @@ -313,22 +289,6 @@ public class VolmitSender implements CommandSender { return MiniMessage.miniMessage().deserialize(C.mini(t)); } - public void showWaiting(String passive, CompletableFuture f) { - // isDone() alone is the completion signal: it covers normal, exceptional AND - // cancelled completion, where the old null-value gate spun forever on a - // CompletableFuture or a failed future. - AtomicInteger v = new AtomicInteger(-1); - v.set(J.ar(() -> { - if (f.isDone()) { - J.car(v.get()); - sendAction(" "); - return; - } - - sendProgress(-1, passive, HudSurface.TITLE, HudSurface.ACTION_BAR); - }, 0)); - } - @Override public void sendMessage(String message) { if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { 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 763e2fb8d..91618e88a 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 @@ -23,18 +23,12 @@ import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.util.common.format.C; 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 art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicLong; public interface Job { String getName(); @@ -73,40 +67,18 @@ public interface Job { default void execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) { PrecisionStopwatch p = PrecisionStopwatch.start(); CompletableFuture f = J.afut(this::execute); - HudSlotClaim titleClaim = sender.isPlayer() - ? BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE))) - : null; - HudSlotClaim barClaim = sender.isPlayer() - ? BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR))) - : null; - AtomicLong lastResolveMs = new AtomicLong(0L); int c = J.ar(() -> { if (sender.isPlayer()) { - long now = System.currentTimeMillis(); - if (now - lastResolveMs.get() >= 250L) { - lastResolveMs.set(now); - titleClaim.resolve(); - barClaim.resolve(); - } - HudSurface titleSurface = titleClaim.granted(); - HudSurface barSurface = barClaim.granted(); - sender.sendProgress(getProgress(), getName(), titleSurface, barSurface); - if (barSurface == HudSurface.BOSS_BAR) { - BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L); - } else if (barSurface == HudSurface.ACTION_BAR) { - BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); - } + sender.sendProgress(getProgress(), getName()); + BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L); } else { sender.sendMessage(getName() + ": " + getProgressString()); } }, sender.isPlayer() ? 0 : 20); f.whenComplete((fs, ff) -> { J.car(c); - if (titleClaim != null) { - titleClaim.release(); - } - if (barClaim != null) { - barClaim.release(); + if (sender.isPlayer()) { + sender.sendAction(" "); BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); } if (!silentMsg) { diff --git a/core/src/main/resources/languages/de_DE.json b/core/src/main/resources/languages/de_DE.json index dca490f37..8f5a91095 100644 --- a/core/src/main/resources/languages/de_DE.json +++ b/core/src/main/resources/languages/de_DE.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cWie wäre es stattdessen mit dem Namen \"IrisWorld\"?", "iris.bukkit.commandiris.that_folder_already_exists": "§cDieser Ordner existiert bereits!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eVersuche eine der folgenden Optionen: overworld, vanilla, flat, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aVorbereitung der Welt abgeschlossen. Iris startet den Server jetzt neu, um \"{worldName}\" zu generieren/zu laden.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cBei der Erstellung ist eine Ausnahme aufgetreten. Weitere Details findest du in der Konsole.", "iris.bukkit.commandiris.successfully_created_your_world": "§aDeine Welt wurde erfolgreich erstellt!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cKonvertierung fehlgeschlagen: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Konvertiert: {get} in {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cEinige Schematic-Dateien konnten nicht konvertiert werden. Weitere Einzelheiten stehen in der Konsole.", - "iris.bukkit.runtime.studiosvc.installing_package": "Installiere Paket: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Suche nach Paket: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{type}.iris im Ordner {WORKSPACENAME} gefunden", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Dimension {type} im Ordner {WORKSPACENAME} gefunden. Sie wird neu gepackt.", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Abschluss", "iris.runtime.studio.stage.cleanup": "Bereinigung", "iris.runtime.world_create.teleport_failed": "§eDie Welt wurde erstellt, aber die automatische Teleportation ist fehlgeschlagen. Versuche /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} Chunks", - "iris.runtime.world_create.console": "§6Generierung §e{percent}%§7 {generated}/{required} Chunks§8 ({remaining} verbleibend)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fVorgenerierung", "iris.runtime.world_create.pregen.console": "§6Vorgenerierung §e{percent}%", "iris.runtime.chunk_job.title.regen": "Regenerieren", diff --git a/core/src/main/resources/languages/es_ES.json b/core/src/main/resources/languages/es_ES.json index 365561766..7983cbaa7 100644 --- a/core/src/main/resources/languages/es_ES.json +++ b/core/src/main/resources/languages/es_ES.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c¿Podemos sugerir el nombre \"IrisWorld\"?", "iris.bukkit.commandiris.that_folder_already_exists": "§c¡Esa carpeta ya existe!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePrueba una de estas opciones: overworld, vanilla, flat, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aPreparación del mundo completada. Iris está reiniciando el servidor para generar/cargar \"{worldName}\".", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cSe produjo una excepción durante la creación. Consulta la consola para obtener más detalles.", "iris.bukkit.commandiris.successfully_created_your_world": "§a¡Tu mundo se creó correctamente!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cNo se pudo convertir: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Convertido: {get} en {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cNo se pudieron convertir algunos schematics. Consulta la consola para obtener más detalles.", - "iris.bukkit.runtime.studiosvc.installing_package": "Instalando pack: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Buscando pack: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Se encontró {type}.iris en la carpeta {WORKSPACENAME}", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Se encontró la dimensión {type} en la carpeta {WORKSPACENAME}. Volviendo a empaquetarla", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Finalizando", "iris.runtime.studio.stage.cleanup": "Limpiando", "iris.runtime.world_create.teleport_failed": "§eEl mundo se creó, pero falló el teletransporte automático. Prueba /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunks", - "iris.runtime.world_create.console": "§6Generando §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} restantes)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregenerando", "iris.runtime.world_create.pregen.console": "§6Pregenerando §e{percent}%", "iris.runtime.chunk_job.title.regen": "Regenerar", diff --git a/core/src/main/resources/languages/fi_FI.json b/core/src/main/resources/languages/fi_FI.json index 0576b6ba4..4b9577a0a 100644 --- a/core/src/main/resources/languages/fi_FI.json +++ b/core/src/main/resources/languages/fi_FI.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cVoimmeko ehdottaa nimeä \"IrisWorldSen sijaan?", "iris.bukkit.commandiris.that_folder_already_exists": "§cSe kansio on jo olemassa!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eKokeile yksi: maapallo, vanilla, litteä, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aMaailman valmistelu valmis. Iris käynnistää palvelimen nyt uudelleen, jotta \"{worldName}\" luodaan/ladataan.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cLuomisen aikana nostettu poikkeus. Katso lisätietoja konsolista.", "iris.bukkit.commandiris.successfully_created_your_world": "§aOnnistuneesti loin maailmasi!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cMuuntaminen epäonnistui: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Muunnettu: {get} sisään {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cJotkut kaaviot eivät muuttuneet. Tarkista konsolista yksityiskohtia.", - "iris.bukkit.runtime.studiosvc.installing_package": "Asennuspaketti: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Etsin pakettia: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Löytyi {type}0, iiris {WORKSPACENAME} kansio", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Löytyi {type} ulottuvuus {WORKSPACENAME} kansio. Uudelleenpakkaaminen", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Viimeistellään", "iris.runtime.studio.stage.cleanup": "Siivotaan", "iris.runtime.world_create.teleport_failed": "§eMaailma luotiin, mutta automaattinen teleportti epäonnistui. Yritä /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunkia", - "iris.runtime.world_create.console": "§6Luodaan §e{percent}%§7 {generated}/{required} chunkia§8 ({remaining} vasen)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fEsituotanto", "iris.runtime.world_create.pregen.console": "§6Esituotanto §e{percent}%", "iris.runtime.chunk_job.title.regen": "Regin", diff --git a/core/src/main/resources/languages/fr_FR.json b/core/src/main/resources/languages/fr_FR.json index b01c327dc..7dab73106 100644 --- a/core/src/main/resources/languages/fr_FR.json +++ b/core/src/main/resources/languages/fr_FR.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cNous vous suggérons plutôt le nom \"IrisWorld\".", "iris.bukkit.commandiris.that_folder_already_exists": "§cCe dossier existe déjà !", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eEssayez l'une des options suivantes : overworld, vanilla, flat, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aPréparation du monde terminée. Iris redémarre maintenant le serveur pour générer/charger \"{worldName}\".", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cUne exception s'est produite pendant la création. Consultez la console pour plus de détails.", "iris.bukkit.commandiris.successfully_created_your_world": "§aVotre monde a été créé avec succès !", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cImpossible de convertir : {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Converti : {get} dans {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cCertains schematics n'ont pas pu être convertis. Consultez la console pour plus de détails.", - "iris.bukkit.runtime.studiosvc.installing_package": "Installation du pack : {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Recherche du pack : {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{type}.iris trouvé dans le dossier {WORKSPACENAME}", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Dimension {type} trouvée dans le dossier {WORKSPACENAME}. Nouvel empaquetage", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Finalisation", "iris.runtime.studio.stage.cleanup": "Nettoyage", "iris.runtime.world_create.teleport_failed": "§eLe monde a été créé, mais la téléportation automatique a échoué. Essayez /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunks", - "iris.runtime.world_create.console": "§6Génération §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} restants)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPrégénération", "iris.runtime.world_create.pregen.console": "§6Prégénération §e{percent}%", "iris.runtime.chunk_job.title.regen": "Régénérer", diff --git a/core/src/main/resources/languages/he_IL.json b/core/src/main/resources/languages/he_IL.json index b9a963927..ddde4e232 100644 --- a/core/src/main/resources/languages/he_IL.json +++ b/core/src/main/resources/languages/he_IL.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cאפשר להציע את השם \"IrisWorld\"במקום?", "iris.bukkit.commandiris.that_folder_already_exists": "§cהתיקיה הזו כבר קיימת!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eנסה: Overworld vanilla, שטוח, TheendXTry", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aהכנת העולם הושלמה. Iris מפעיל כעת מחדש את השרת כדי ליצור/לטעון את \"{worldName}\".", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cלמעטים שגדלו במהלך הבריאה. ראו את הקונסולה לפרטים נוספים.", "iris.bukkit.commandiris.successfully_created_your_world": "§aיצרתם בהצלחה את עולמכם!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cנכשל להמיר: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7המונחים: {get} פנימה {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cכמה סנטימטרים לא הצליחו להמיר. בדוק את הקונסולה לפרטים", - "iris.bukkit.runtime.studiosvc.installing_package": "התקנת חבילה: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "מחפש חבילה: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "נמצאו {type}חרירי {WORKSPACENAME} התיקיה", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "נמצאו {type} מימד {WORKSPACENAME} תיקיה. Repacking", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "מסיים", "iris.runtime.studio.stage.cleanup": "מנקה", "iris.runtime.world_create.teleport_failed": "§eהעולם נברא, אך התקשורת האוטומטית נכשלה. נסו לנסות /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} צ'אנקים", - "iris.runtime.world_create.console": "§6ייצור §e{percent}%§7 {generated}/{required} צ'אנקים§8 ({remaining} שמאל)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fהפקה מוקדמת", "iris.runtime.world_create.pregen.console": "§6הפקה מוקדמת §e{percent}%", "iris.runtime.chunk_job.title.regen": "מחדש", diff --git a/core/src/main/resources/languages/it_IT.json b/core/src/main/resources/languages/it_IT.json index 1871dd82b..07a354a0c 100644 --- a/core/src/main/resources/languages/it_IT.json +++ b/core/src/main/resources/languages/it_IT.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cPossiamo suggerire il nome \"IrisWorld\" invece?", "iris.bukkit.commandiris.that_folder_already_exists": "§cQuella cartella esiste già!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProva una delle seguenti opzioni: overworld, vanilla, flat, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aPreparazione del mondo completata. Iris sta riavviando il server per generare/caricare \"{worldName}\".", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cSi è verificata un'eccezione durante la creazione. Consulta la console per maggiori dettagli.", "iris.bukkit.commandiris.successfully_created_your_world": "§aCreato con successo il tuo mondo!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cConversione non riuscita: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Convertito: {get} nel {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cNon è stato possibile convertire alcuni file schematic. Controlla la console per i dettagli.", - "iris.bukkit.runtime.studiosvc.installing_package": "Installazione del pacchetto: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Ricerca del pacchetto: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Trovato {type}.iris nella cartella {WORKSPACENAME}", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Trovata la dimensione {type} nella cartella {WORKSPACENAME}. Creazione del nuovo pacchetto.", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Finalizzazione", "iris.runtime.studio.stage.cleanup": "Pulizia", "iris.runtime.world_create.teleport_failed": "§eIl mondo è stato creato, ma il teletrasporto automatico non è riuscito. Prova /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunk", - "iris.runtime.world_create.console": "§6Generazione §e{percent}%§7 {generated}/{required} chunk§8 ({remaining} rimanenti)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregenerazione", "iris.runtime.world_create.pregen.console": "§6Pregenerazione §e{percent}%", "iris.runtime.chunk_job.title.regen": "Rigenera", diff --git a/core/src/main/resources/languages/ja-JP.json b/core/src/main/resources/languages/ja-JP.json index be3cae3e7..e842a357b 100644 --- a/core/src/main/resources/languages/ja-JP.json +++ b/core/src/main/resources/languages/ja-JP.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c代わりに \"IrisWorld\" という名前はいかがですか?", "iris.bukkit.commandiris.that_folder_already_exists": "§cそのフォルダーはすでに存在します!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e次のいずれかを指定してください: overworld、vanilla、flat、theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aワールドの準備が完了しました。Iris は \"{worldName}\" を生成/読み込むためにサーバーを再起動しています。", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§c作成中に例外が発生しました。詳細はコンソールを確認してください。", "iris.bukkit.commandiris.successfully_created_your_world": "§aワールドを作成しました!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§c変換に失敗しました: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7変換完了: {get}({value})", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c一部の schematic を変換できませんでした。詳細はコンソールを確認してください。", - "iris.bukkit.runtime.studiosvc.installing_package": "パッケージのインストール: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "パッケージを探します: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} フォルダーに {type}.iris が見つかりました", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "{WORKSPACENAME} フォルダーに {type} ディメンションが見つかりました。再パッケージしています", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "完了処理中", "iris.runtime.studio.stage.cleanup": "クリーンアップ中", "iris.runtime.world_create.teleport_failed": "§eワールドを作成しましたが、自動テレポートに失敗しました。/iris teleport world={world} をお試しください", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} チャンク", - "iris.runtime.world_create.console": "§6生成中 §e{percent}%§7 {generated}/{required} チャンク§8(残り {remaining})", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f事前生成中", "iris.runtime.world_create.pregen.console": "§6事前生成中 §e{percent}%", "iris.runtime.chunk_job.title.regen": "再生成", diff --git a/core/src/main/resources/languages/ko_KR.json b/core/src/main/resources/languages/ko_KR.json index c97277c6c..6f2c3ae7d 100644 --- a/core/src/main/resources/languages/ko_KR.json +++ b/core/src/main/resources/languages/ko_KR.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c우리는 이름 \"을 제안 할 수있다IrisWorld\" 대신?", "iris.bukkit.commandiris.that_folder_already_exists": "§c그 폴더는 이미 존재합니다!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e중 하나를 시도: overworld, 바닐라, 플랫, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§a월드 준비가 완료되었습니다. Iris가 \"{worldName}\"을 생성/로드하기 위해 서버를 재시작하고 있습니다.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§c창조 중 발생한 예외. 자세한 내용은 콘솔을 참조하십시오.", "iris.bukkit.commandiris.successfully_created_your_world": "§a세상을 성공적으로 만들었습니다!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§c변환 실패: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7변환 완료: {get}, 소요 {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c일부 설계도 변환 실패. 자세한 내용은 콘솔을 확인하십시오.", - "iris.bukkit.runtime.studiosvc.installing_package": "패키지 설치 중: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "포장을 위한 보기: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} 폴더에서 {type}.iris를 찾았습니다", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "{WORKSPACENAME} 폴더에서 {type} 차원을 찾았습니다. 다시 패키징합니다", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "마무리 중", "iris.runtime.studio.stage.cleanup": "정리 중", "iris.runtime.world_create.teleport_failed": "§e월드를 만들었지만 자동 순간이동에 실패했습니다. /iris teleport world={world}을(를) 사용해 보세요", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} 청크", - "iris.runtime.world_create.console": "§6생성 중 §e{percent}%§7 {generated}/{required} 청크§8 ({remaining} 남음)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f연구분야", "iris.runtime.world_create.pregen.console": "§6연구분야 §e{percent}%", "iris.runtime.chunk_job.title.regen": "재생성", diff --git a/core/src/main/resources/languages/lt_LT.json b/core/src/main/resources/languages/lt_LT.json index fc3fd4d06..2796b75d3 100644 --- a/core/src/main/resources/languages/lt_LT.json +++ b/core/src/main/resources/languages/lt_LT.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cAr galime pasiūlyti pavadinimą \"IrisWorld\"vietoj to?", "iris.bukkit.commandiris.that_folder_already_exists": "§cTas aplankas jau egzistuoja!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePabandykite vieną iš: per pasaulį, vanilla, butas, pabaiga", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aPasaulio paruošimas baigtas. Iris dabar paleidžia serverį iš naujo, kad sugeneruotų / įkeltų „{worldName}“.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cSukūrimo metu iškelta išimtis. Daugiau informacijos rasite konsolėje.", "iris.bukkit.commandiris.successfully_created_your_world": "§aSėkmingai sukūrė savo pasaulį!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cNepavyko konvertuoti: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Konvertuotas: {get} yra {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cKai schemos nepavyko konvertuoti. Patikrinkite konsolės detales.", - "iris.bukkit.runtime.studiosvc.installing_package": "Įdiegiamas paketas: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Ieškoma paketo: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Aplanke {WORKSPACENAME} rastas {type}.iris", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Rasta {type} dimensija {WORKSPACENAME} aplankas. Perpakavimas", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Užbaigiama", "iris.runtime.studio.stage.cleanup": "Valoma", "iris.runtime.world_create.teleport_failed": "§ePasaulis buvo sukurtas, bet automatinis teleportas nepavyko. Stenkitės /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunkai", - "iris.runtime.world_create.console": "§6Generuojama §e{percent}%§7 {generated}/{required} chunkai§8 ({remaining} kairėje)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fRegeneravimas", "iris.runtime.world_create.pregen.console": "§6Regeneravimas §e{percent}%", "iris.runtime.chunk_job.title.regen": "Režen", diff --git a/core/src/main/resources/languages/nl_NL.json b/core/src/main/resources/languages/nl_NL.json index 126eb8738..ef9146cc3 100644 --- a/core/src/main/resources/languages/nl_NL.json +++ b/core/src/main/resources/languages/nl_NL.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cMogen we de naam voorstellen \"IrisWorldIn plaats daarvan?", "iris.bukkit.commandiris.that_folder_already_exists": "§cDie map bestaat al!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProbeer een van: overwereld, vanilla, plat, einde", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aVoorbereiding van de wereld voltooid. Iris herstart de server nu om \"{worldName}\" te genereren/laden.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cUitzondering bij de schepping. Zie de console voor meer details.", "iris.bukkit.commandiris.successfully_created_your_world": "§aSuccesvol jullie wereld gecreëerd!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cConverteren mislukt: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Omgezet: {get} de {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cSommige schema's konden niet worden omgezet. Controleer de console voor details.", - "iris.bukkit.runtime.studiosvc.installing_package": "Pakket installeren: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Op zoek naar pakket: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Gevonden {type}Iris in {WORKSPACENAME} map", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Gevonden {type} dimensie in {WORKSPACENAME} map. Herverpakking", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Afronden", "iris.runtime.studio.stage.cleanup": "Opruimen", "iris.runtime.world_create.teleport_failed": "§eDe wereld werd gecreëerd, maar de automatische teleportatie mislukte. Probeer /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunks", - "iris.runtime.world_create.console": "§6Genereren §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} links)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneren", "iris.runtime.world_create.pregen.console": "§6Pregeneren §e{percent}%", "iris.runtime.chunk_job.title.regen": "RegenName", diff --git a/core/src/main/resources/languages/pl_PL.json b/core/src/main/resources/languages/pl_PL.json index 610e59eda..64426952d 100644 --- a/core/src/main/resources/languages/pl_PL.json +++ b/core/src/main/resources/languages/pl_PL.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cCzy możemy zasugerować nazwę \"IrisWorld\"zamiast?", "iris.bukkit.commandiris.that_folder_already_exists": "§cTen folder już istnieje!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eSpróbuj jeden z: zaświaty, vanilla, płaski, koniec", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aPrzygotowanie świata ukończone. Iris uruchamia teraz serwer ponownie, aby wygenerować/wczytać „{worldName}“.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cWyjątek podniesiony podczas tworzenia. Więcej szczegółów znajdziesz w konsoli.", "iris.bukkit.commandiris.successfully_created_your_world": "§aZ powodzeniem stworzyłeś swój świat!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cNie można przekonwertować: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Przekonwertowany: {get} w {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cNiektóre schematy nie mogły się zmienić. Sprawdź w konsoli szczegóły.", - "iris.bukkit.runtime.studiosvc.installing_package": "Instalacja pakietu: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Szukam pakietu: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Znaleziono {type}.iris w {WORKSPACENAME} katalog", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Znaleziono {type} wymiar {WORKSPACENAME} teczka. Przepakowywanie", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Finalizowanie", "iris.runtime.studio.stage.cleanup": "Czyszczenie", "iris.runtime.world_create.teleport_failed": "§eŚwiat został stworzony, ale automatyczny teleport zawiódł. Spróbuj /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunki", - "iris.runtime.world_create.console": "§6Generowanie §e{percent}%§7 {generated}/{required} chunki§8 ({remaining} lewa)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneracja", "iris.runtime.world_create.pregen.console": "§6Pregeneracja §e{percent}%", "iris.runtime.chunk_job.title.regen": "Regen.", diff --git a/core/src/main/resources/languages/pt_PT.json b/core/src/main/resources/languages/pt_PT.json index b20ff8e49..92414fe6f 100644 --- a/core/src/main/resources/languages/pt_PT.json +++ b/core/src/main/resources/languages/pt_PT.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cPodemos sugerir o nome \"IrisWorld\" em vez disso?", "iris.bukkit.commandiris.that_folder_already_exists": "§cEssa pasta já existe!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eExperimente um de: overworld, vanilla, flat, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aPreparação do mundo concluída. O Iris está agora a reiniciar o servidor para gerar/carregar \"{worldName}\".", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cExceção levantada durante a criação. Veja o console para mais detalhes.", "iris.bukkit.commandiris.successfully_created_your_world": "§aCriou com sucesso o seu mundo!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cFalha ao converter: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Convertido: {get} em {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cAlguns esquemas não conseguiram converter. Verifique o console para obter detalhes.", - "iris.bukkit.runtime.studiosvc.installing_package": "Instalando o Pacote: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Procurando pelo pacote: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Encontrado {type}.iris em {WORKSPACENAME} pasta", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Encontrado {type} dimensão em {WORKSPACENAME} pasta. Reembalagem", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "A finalizar", "iris.runtime.studio.stage.cleanup": "A limpar", "iris.runtime.world_create.teleport_failed": "§eO mundo foi criado, mas o teletransporte automático falhou. Tenta. /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunks", - "iris.runtime.world_create.console": "§6Gerando §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} esquerda)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneração", "iris.runtime.world_create.pregen.console": "§6Pregeneração §e{percent}%", "iris.runtime.chunk_job.title.regen": "Regen.", diff --git a/core/src/main/resources/languages/ru_RU.json b/core/src/main/resources/languages/ru_RU.json index 1142e2d7c..2f680468f 100644 --- a/core/src/main/resources/languages/ru_RU.json +++ b/core/src/main/resources/languages/ru_RU.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cМожно предложить название \"IrisWorldВместо этого?", "iris.bukkit.commandiris.that_folder_already_exists": "§cЭта папка уже существует!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eПопробуйте один из вариантов: overworld, vanilla, flat, theend", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aПодготовка мира завершена. Iris перезапускает сервер, чтобы сгенерировать/загрузить «{worldName}».", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cИсключение, возникшее при сотворении. Смотрите консоль для более подробной информации.", "iris.bukkit.commandiris.successfully_created_your_world": "§aУспешно создал свой мир!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cНе удалось конвертировать: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Преобразовано: {get} в {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cНекоторые схемы не удалось конвертировать. Проверьте консоль для деталей.", - "iris.bukkit.runtime.studiosvc.installing_package": "Установка пакета: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "В поисках пакета: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Найден {type}.iris в {WORKSPACENAME} папка", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Найден {type} измерение {WORKSPACENAME} папка. Упаковка", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Завершение", "iris.runtime.studio.stage.cleanup": "Очистка", "iris.runtime.world_create.teleport_failed": "§eМир был создан, но автоматический телепорт провалился. Попробуйте /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} чанки", - "iris.runtime.world_create.console": "§6генерировать §e{percent}%§7 {generated}/{required} чанки§8 ({remaining} осталось)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fгенерирующий", "iris.runtime.world_create.pregen.console": "§6генерирующий §e{percent}%", "iris.runtime.chunk_job.title.regen": "Реген", diff --git a/core/src/main/resources/languages/tr_TR.json b/core/src/main/resources/languages/tr_TR.json index 786db0c4c..2c7fe0b50 100644 --- a/core/src/main/resources/languages/tr_TR.json +++ b/core/src/main/resources/languages/tr_TR.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cAdımı önerebiliriz \"IrisWorldBunun yerine?", "iris.bukkit.commandiris.that_folder_already_exists": "§cBu klasör zaten var!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eBirini deneyin: Overworld, vanilla, düz, son", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aDünya hazırlığı tamamlandı. Iris, \"{worldName}\" dünyasını oluşturmak/yüklemek için sunucuyu yeniden başlatıyor.", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cYaratılış sırasında ortaya çıktı. Konsolu daha fazla ayrıntı için görün.", "iris.bukkit.commandiris.successfully_created_your_world": "§aBaşarılı bir şekilde dünyanızı yarattı!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cdönüştürmeye başarısız oldu: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Dönüştürüldü: {get}, {value} sürede", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cBazı şematik dönüştürmeye başarısız oldu. Bilgi için konsolu kontrol edin.", - "iris.bukkit.runtime.studiosvc.installing_package": "Yükleme Paketi: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Paket arıyorsunuz: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} klasöründe {type}.iris bulundu", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Bul bulundu {type} boyutta boyut {WORKSPACENAME} klasörü. Repacking", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Tamamlanıyor", "iris.runtime.studio.stage.cleanup": "Temizleniyor", "iris.runtime.world_create.teleport_failed": "§eDünya yaratıldı, ancak otomatik telgraf başarısız oldu. Deneyin /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} chunk", - "iris.runtime.world_create.console": "§6Genating §e{percent}%§7 {generated}/{required} chunk§8 ({remaining} sol)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fPregeneating", "iris.runtime.world_create.pregen.console": "§6Pregeneating §e{percent}%", "iris.runtime.chunk_job.title.regen": "Rejen", diff --git a/core/src/main/resources/languages/vi_VI.json b/core/src/main/resources/languages/vi_VI.json index 6551dfac2..bcd8cdd30 100644 --- a/core/src/main/resources/languages/vi_VI.json +++ b/core/src/main/resources/languages/vi_VI.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cChúng ta có thể đề nghị cái tên \"IrisWorldThay vào đó?", "iris.bukkit.commandiris.that_folder_already_exists": "§cThư mục đó đã có!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eHãy thử một trong những: ngoài thế giới, vanilla, bằng phẳng, kết thúc", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aĐã chuẩn bị xong thế giới. Iris đang khởi động lại máy chủ để tạo/tải \"{worldName}\".", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cNgoại lệ được nuôi lớn trong suốt quá trình sáng tạo. Xem bảng điều khiển để biết thêm chi tiết.", "iris.bukkit.commandiris.successfully_created_your_world": "§aThành công trong việc tạo ra thế giới của anh!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§cLỗi chuyển đổi: {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Chuyển đổi: {get} trong {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cMột số sơ đồ không thể chuyển đổi. Kiểm tra bảng điều khiển xem có chi tiết gì không.", - "iris.bukkit.runtime.studiosvc.installing_package": "Đang cài đặt gói: {name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "Đang tìm gói: {type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Tìm thấy {type}Tham gia {WORKSPACENAME} Thư mục", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Tìm thấy {type} Không gian trong {WORKSPACENAME} thư mục. Đang nạp lại", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "Đang hoàn tất", "iris.runtime.studio.stage.cleanup": "Đang dọn dẹp", "iris.runtime.world_create.teleport_failed": "§eThế giới đã được tạo ra, nhưng dịch chuyển tự động thất bại. Thử /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} Chunk", - "iris.runtime.world_create.console": "§6Đang tạo ra §e{percent}%§7 {generated}/{required} Chunk§8 ({remaining} trái)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §fTạo ra", "iris.runtime.world_create.pregen.console": "§6Tạo ra §e{percent}%", "iris.runtime.chunk_job.title.regen": "Bản sao", diff --git a/core/src/main/resources/languages/zh_CN.json b/core/src/main/resources/languages/zh_CN.json index e0e5986f7..8df60edaf 100644 --- a/core/src/main/resources/languages/zh_CN.json +++ b/core/src/main/resources/languages/zh_CN.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c建议改用名称 \"IrisWorld\"。", "iris.bukkit.commandiris.that_folder_already_exists": "§c该文件夹已存在!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可尝试:overworld、vanilla、flat 或 the_end", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§a世界暂存完成。Iris 正在重启服务器以生成并加载 \"{worldName}\"。", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§c创建期间发生异常。详情请查看控制台。", "iris.bukkit.commandiris.successfully_created_your_world": "§a世界创建成功!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§c转换失败 : {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7已转换 : {get} 输入 {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c一些示意图未能转换. 检查控制台的细节。", - "iris.bukkit.runtime.studiosvc.installing_package": "正在安装内容包:{name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "正在查找内容包:{type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "在 {WORKSPACENAME} 文件夹中找到 {type}.iris", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "在 {WORKSPACENAME} 文件夹中找到 {type} 维度,正在重新打包", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "最后确定", "iris.runtime.studio.stage.cleanup": "清理", "iris.runtime.world_create.teleport_failed": "§e世界是创建的,但自动电传传送失败. 尝试 /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} 块", - "iris.runtime.world_create.console": "§6正在生成 §e{percent}%§7 {generated}/{required} 块§8 ({remaining} 左侧)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f预生成", "iris.runtime.world_create.pregen.console": "§6预生成 §e{percent}%", "iris.runtime.chunk_job.title.regen": "瑞根,你好吗?", diff --git a/core/src/main/resources/languages/zh_TW.json b/core/src/main/resources/languages/zh_TW.json index 4dccb0496..25ec75286 100644 --- a/core/src/main/resources/languages/zh_TW.json +++ b/core/src/main/resources/languages/zh_TW.json @@ -149,6 +149,8 @@ "iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c建議改用名稱 \"IrisWorld\"。", "iris.bukkit.commandiris.that_folder_already_exists": "§c該資料夾已存在!", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可嘗試:overworld、vanilla、flat 或 the_end", + "iris.bukkit.commandiris.dimension_not_found": "§cCould not find dimension §f{dimension}§c.", + "iris.bukkit.commandiris.install_pack_and_restart": "§eInstall it with §b{command}§e and restart the server.", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§a世界暫存完成。Iris 正在重新啟動伺服器以生成並載入 \"{worldName}\"。", "iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§c建立期間發生例外。詳細資訊請查看主控台。", "iris.bukkit.commandiris.successfully_created_your_world": "§a世界建立成功!", @@ -1132,7 +1134,9 @@ "iris.bukkit.runtime.irisconverter.failed_convert": "§c轉換失敗 : {name}", "iris.bukkit.runtime.irisconverter.converted_3": "§7已轉換 : {get} 輸入 {value}", "iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c一些示意圖未能轉換. 檢查控制檯的細節。", - "iris.bukkit.runtime.studiosvc.installing_package": "正在安裝內容包:{name}:{loadKey}", + "iris.bukkit.runtime.studiosvc.installing_package": "§6World pack §b{name}:{loadKey}§7 | §fPublishing snapshot", + "iris.bukkit.runtime.studiosvc.pack_copy_requires_async_thread": "§cIris refused to copy the world pack on the Bukkit primary thread.", + "iris.bukkit.runtime.studiosvc.pack_install_failed": "§cFailed to install world pack §f{dimension}§c: {error}", "iris.bukkit.runtime.studiosvc.looking_package": "正在尋找內容包:{type}", "iris.bukkit.runtime.studiosvc.found_iris_folder": "在 {WORKSPACENAME} 資料夾中找到 {type}.iris", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "在 {WORKSPACENAME} 資料夾中找到 {type} 維度,正在重新打包", @@ -1203,8 +1207,28 @@ "iris.runtime.studio.stage.finalize_open": "最後確定", "iris.runtime.studio.stage.cleanup": "清理", "iris.runtime.world_create.teleport_failed": "§e世界是建立的,但自動電傳傳送失敗. 嘗試 /iris teleport world={world}", - "iris.runtime.world_create.action": "{bar}§7 §e{percent}%§8 {generated}/{required} 塊", - "iris.runtime.world_create.console": "§6正在生成 §e{percent}%§7 {generated}/{required} 塊§8 ({remaining} 左側)", + "iris.runtime.world_create.bossbar.working": "§6World §b{world}§7 | §fStarting", + "iris.runtime.world_create.bossbar.progress": "§6World §b{world}§7 | §e{percent}% §f{stage}", + "iris.runtime.world_create.bossbar.failed": "§6World §b{world}§7 | §cFAILED §8{percent}%", + "iris.runtime.world_create.bossbar.ready": "§6World §b{world}§7 | §aREADY 100%", + "iris.runtime.world_create.lifecycle.action": "{bar}§7 §e{percent}%§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.failed": "{bar}§7 §cFAILED§7 | §f{stage}{detail}§8 {elapsed}", + "iris.runtime.world_create.lifecycle.action.ready": "{bar}§7 §a100%§7 | §aWorld ready§8 {elapsed}", + "iris.runtime.world_create.lifecycle.console": "§6World §b{world} {bar} §e{percent}%§7 {stage}{detail}§8 ({elapsed})", + "iris.runtime.world_create.lifecycle.console.failed": "§6World §b{world}§7 | §ccreation failed§8 after {elapsed}", + "iris.runtime.world_create.lifecycle.console.ready": "§6World §b{world}§7 | §aready§8 in {elapsed}", + "iris.runtime.world_create.stage.initializing": "Initializing", + "iris.runtime.world_create.stage.resolve_dimension": "Resolving dimension", + "iris.runtime.world_create.stage.validate_pack": "Validating pack", + "iris.runtime.world_create.stage.prepare_world_pack": "Preparing world pack", + "iris.runtime.world_create.stage.install_datapacks": "Installing datapacks", + "iris.runtime.world_create.stage.prepare_generator": "Preparing generator", + "iris.runtime.world_create.stage.create_world": "Generating spawn", + "iris.runtime.world_create.stage.register_world": "Registering world", + "iris.runtime.world_create.stage.teleport_player": "Finding safe entry", + "iris.runtime.world_create.stage.pregenerate": "Pregenerating", + "iris.runtime.world_create.stage.finalize": "Finalizing", + "iris.runtime.world_create.stage.complete": "World ready", "iris.runtime.world_create.pregen.action": "{bar}§7 §e{percent}%§7 | §f預生成", "iris.runtime.world_create.pregen.console": "§6預生成 §e{percent}%", "iris.runtime.chunk_job.title.regen": "瑞根,你好嗎?", diff --git a/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java b/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java index 7a8b5dd10..468c621ae 100644 --- a/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java +++ b/core/src/test/java/art/arcane/iris/core/IrisDatapackCompilerTest.java @@ -16,9 +16,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -188,6 +190,68 @@ public class IrisDatapackCompilerTest { assertFalse(Files.exists(datapackRoot.resolve("data/minecraft/dimension/the_end.json"))); } + @Test + public void equivalentFrozenPackAndLevelStemBindingDoNotChangeRegistryRequirements() throws Exception { + Path installedPack = temporaryFolder.newFolder("registry-installed-pack").toPath(); + Path frozenPack = temporaryFolder.newFolder("registry-frozen-pack").toPath(); + createPack(installedPack, "overworld", "forest_custom"); + createPack(frozenPack, "overworld", "forest_custom"); + DataFixerV1217 fixer = new DataFixerV1217(); + + Map loaded = IrisDatapackCompiler.computeRegistryRequirements( + List.of(installedPack.toFile()), + fixer); + Map afterWorldCreation = IrisDatapackCompiler.computeRegistryRequirements( + List.of(installedPack.toFile(), frozenPack.toFile()), + fixer); + String loadedCompilerInputs = IrisDatapackCompiler.computeInputFingerprint( + List.of(installedPack.toFile()), + List.of(), + false, + "compiler-a"); + String afterWorldCreationCompilerInputs = IrisDatapackCompiler.computeInputFingerprint( + List.of(installedPack.toFile(), frozenPack.toFile()), + List.of(binding("ow", "overworld")), + false, + "compiler-a"); + + assertNotEquals(loadedCompilerInputs, afterWorldCreationCompilerInputs); + assertEquals(loaded, afterWorldCreation); + assertTrue(ServerConfigurator.loadedRegistrySatisfies(loaded, afterWorldCreation)); + } + + @Test + public void changedDimensionRegistryRequirementsDoNotReuseLoadedRuntime() throws Exception { + Path pack = temporaryFolder.newFolder("registry-changed-pack").toPath(); + createPack(pack, "overworld", "forest_custom", "NORMAL"); + DataFixerV1217 fixer = new DataFixerV1217(); + Map loaded = IrisDatapackCompiler.computeRegistryRequirements( + List.of(pack.toFile()), + fixer); + + createPack(pack, "overworld", "forest_custom", "NETHER"); + Map changedDimensionType = IrisDatapackCompiler.computeRegistryRequirements( + List.of(pack.toFile()), + fixer); + assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedDimensionType)); + + createPack(pack, "overworld", "new_custom", "NORMAL"); + Map changedCustomBiome = IrisDatapackCompiler.computeRegistryRequirements( + List.of(pack.toFile()), + fixer); + assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedCustomBiome)); + + createPack(pack, "overworld", "forest_custom", "NORMAL"); + Path biomeFile = pack.resolve("biomes/test.json"); + String taggedBiome = Files.readString(biomeFile, StandardCharsets.UTF_8) + .replace("\"id\": \"forest_custom\"", "\"id\": \"forest_custom\", \"tags\": [\"is_hot\"]"); + Files.writeString(biomeFile, taggedBiome, StandardCharsets.UTF_8); + Map changedBiomeTags = IrisDatapackCompiler.computeRegistryRequirements( + List.of(pack.toFile()), + fixer); + assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedBiomeTags)); + } + @Test public void rejectsBindingToMissingDimension() throws Exception { Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath(); diff --git a/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFingerprintTest.java b/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFingerprintTest.java index 3c1348871..4bc82f45b 100644 --- a/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFingerprintTest.java +++ b/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFingerprintTest.java @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -428,6 +429,26 @@ public class ServerConfiguratorDatapackFingerprintTest { assertFalse(ServerConfigurator.reusableRuntimeFingerprint(null, "abc")); } + @Test + public void loadedRegistryAllowsUnrelatedEntriesButRequiresExactRequestedContent() { + Map loaded = Map.of( + "dimension_type/iris:overworld", "dimension-a", + "worldgen/biome/overworld:forest", "biome-a", + "dimension/iris:ow", "level-stem-a"); + + assertTrue(ServerConfigurator.loadedRegistrySatisfies( + loaded, + Map.of( + "dimension_type/iris:overworld", "dimension-a", + "worldgen/biome/overworld:forest", "biome-a"))); + assertFalse(ServerConfigurator.loadedRegistrySatisfies( + loaded, + Map.of("dimension_type/iris:overworld", "dimension-b"))); + assertFalse(ServerConfigurator.loadedRegistrySatisfies( + loaded, + Map.of("worldgen/biome/overworld:new", "biome-new"))); + } + @Test public void restoredCompilerInputFingerprintRequiresReadyNonRestartingRuntime() throws Exception { Field ready = ServerConfigurator.class.getDeclaredField("loadedDatapackRuntimeReady"); diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java index f92126efb..a95fee979 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackBiomeLayerValidatorTest.java @@ -30,11 +30,9 @@ public class PackBiomeLayerValidatorTest { } @Test - public void respectsSingleEntryDefaultsWhenFieldsAreAbsent() throws Exception { + public void respectsIndependentDefaultsWhenFieldsAreAbsent() throws Exception { File pack = temporaryFolder.newFolder("pack"); - // Both fields default to one entry, so omitting both is legal... write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}"); - // ...and two ceiling entries against the implicit single default layer is not. write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}"); assertEquals(List.of( @@ -75,6 +73,40 @@ public class PackBiomeLayerValidatorTest { "Biome 'biome' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries.")); } + @Test + public void rejectsEmbeddedDecoratorWithoutPalette() throws Exception { + File pack = temporaryFolder.newFolder("pack"); + write(pack, "biomes/bad.json", "{\"decorators\":[{\"block\":\"minecraft:magma_block\"}]}"); + + assertEquals(List.of( + "Biome 'bad' decorators[0] must declare a non-empty palette." + ), PackBiomeLayerValidator.validateDecoratorPalettes( + new File(pack, "biomes"), new File(pack, "snippet/decorator"))); + } + + @Test + public void rejectsDecoratorSnippetWithoutPalette() throws Exception { + File pack = temporaryFolder.newFolder("pack"); + write(pack, "snippet/decorator/bad.json", "{\"chance\":0.5}"); + + assertEquals(List.of( + "Decorator snippet 'bad' must declare a non-empty palette." + ), PackBiomeLayerValidator.validateDecoratorPalettes( + new File(pack, "biomes"), new File(pack, "snippet/decorator"))); + } + + @Test + public void acceptsDecoratorPalettesAndSnippetReferences() throws Exception { + File pack = temporaryFolder.newFolder("pack"); + write(pack, "biomes/ok.json", + "{\"decorators\":[{\"palette\":[{\"block\":\"minecraft:magma_block\"}]},\"snippet/decorator/ok\"]}"); + write(pack, "snippet/decorator/ok.json", + "{\"palette\":[{\"block\":\"minecraft:magma_block\"}]}"); + + assertTrue(PackBiomeLayerValidator.validateDecoratorPalettes( + new File(pack, "biomes"), new File(pack, "snippet/decorator")).isEmpty()); + } + private void write(File root, String relative, String content) throws Exception { Path target = new File(root, relative).toPath(); Files.createDirectories(target.getParent()); diff --git a/core/src/test/java/art/arcane/iris/core/runtime/StudioOpenCoordinatorOpenKindTest.java b/core/src/test/java/art/arcane/iris/core/runtime/StudioOpenCoordinatorOpenKindTest.java index 89fd23db1..808841bf3 100644 --- a/core/src/test/java/art/arcane/iris/core/runtime/StudioOpenCoordinatorOpenKindTest.java +++ b/core/src/test/java/art/arcane/iris/core/runtime/StudioOpenCoordinatorOpenKindTest.java @@ -132,6 +132,31 @@ public class StudioOpenCoordinatorOpenKindTest { assertTrue(method.contains("abandonment.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS")); } + @Test + public void queuedRestartDefersFailedOpenCleanupWithoutAcquiringALiveCloseLease() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")); + int openCatch = source.indexOf("} catch (Throwable e) {"); + int restartCheck = source.indexOf( + ".active(LifecycleOperationCoordinator.Domain.SERVER_LIFECYCLE)", + openCatch); + int restartDeferral = source.indexOf( + "deferFailedOpenCleanupToRestart(", + restartCheck); + int liveCleanup = source.indexOf("cleanupFailedOpen(", restartDeferral); + int methodStart = source.indexOf( + "private void deferFailedOpenCleanupToRestart(", + restartDeferral); + int methodEnd = source.indexOf("private boolean transientWorldStorageExists", methodStart); + String method = source.substring(methodStart, methodEnd); + + assertTrue(restartCheck > openCatch); + assertTrue(restartDeferral > restartCheck); + assertTrue(liveCleanup > restartDeferral); + assertTrue(method.contains("queueStartupCleanup(")); + assertFalse(method.contains("closeWorldCoordinated(")); + } + @Test public void openFinalizerReturnsToTheServerThreadBeforeCompletion() throws Exception { String source = Files.readString(Path.of( diff --git a/core/src/test/java/art/arcane/iris/core/service/PackDownloadProgressReporterTest.java b/core/src/test/java/art/arcane/iris/core/service/PackDownloadProgressReporterTest.java index e41e80abf..d6bca1820 100644 --- a/core/src/test/java/art/arcane/iris/core/service/PackDownloadProgressReporterTest.java +++ b/core/src/test/java/art/arcane/iris/core/service/PackDownloadProgressReporterTest.java @@ -183,20 +183,20 @@ public class PackDownloadProgressReporterTest { } @Test - public void playerHudUsesArbitratedActionAndBossBarLanesWithCleanup() throws Exception { + public void playerHudAlwaysShowsLoaderLaneAndActionBarWithCleanup() throws Exception { String source = Files.readString(Path.of( "src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java" )); - assertTrue(source.contains("new HudSlotRequest(")); - assertTrue(source.contains("List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)")); + assertFalse(source.contains("HudSlotRequest")); + assertFalse(source.contains("HudSurface")); assertTrue(source.contains("J.ar(this::pulseHud, HUD_PULSE_TICKS)")); - assertTrue(source.contains("HUD_CLAIM_TTL_MILLIS")); + assertTrue(source.contains("BukkitPlatform.hudLanes().show(")); + assertTrue(source.contains("sender.sendAction(snapshot.line())")); assertTrue(source.contains("HUD_TERMINAL_TICKS, retiredCleanup")); assertTrue(source.contains("BukkitPlatform.hudLanes().retire(playerId, hudLaneId)")); - assertTrue(source.contains("claim.retire();")); + assertTrue(source.contains("BukkitPlatform.hudLanes().hide(player, hudLaneId)")); assertFalse(source.contains("J.runGlobal(cleanup)")); - assertTrue(source.contains("claim.release();")); assertTrue(source.contains("J.car(activeTaskId);")); } diff --git a/core/src/test/java/art/arcane/iris/core/service/StudioSVCWorldPackFeedbackContractTest.java b/core/src/test/java/art/arcane/iris/core/service/StudioSVCWorldPackFeedbackContractTest.java new file mode 100644 index 000000000..e59acdfbc --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/service/StudioSVCWorldPackFeedbackContractTest.java @@ -0,0 +1,33 @@ +package art.arcane.iris.core.service; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class StudioSVCWorldPackFeedbackContractTest { + @Test + public void persistentPackCopyUsesLocalizedStyledOperatorFeedback() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/service/StudioSVC.java" + )); + + assertEquals(1, occurrences(source, "STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD")); + assertEquals(2, occurrences(source, "STUDIO_S_V_C_PACK_INSTALL_FAILED")); + assertFalse(source.contains("sender.sendMessage(\"Iris refused to copy a pack")); + assertFalse(source.contains("sender.sendMessage(\"Failed to install studio pack")); + } + + private static int occurrences(String value, String match) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(match, offset)) >= 0) { + count++; + offset += match.length(); + } + return count; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/tools/IrisCreatorProgressContractTest.java b/core/src/test/java/art/arcane/iris/core/tools/IrisCreatorProgressContractTest.java new file mode 100644 index 000000000..545596d9c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/tools/IrisCreatorProgressContractTest.java @@ -0,0 +1,46 @@ +package art.arcane.iris.core.tools; + +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class IrisCreatorProgressContractTest { + @Test + public void persistentCreateReportsTheWholeLifecycleInsteadOfOnlySpawnChunks() throws Exception { + String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/tools/IrisCreator.java")); + + int startReporter = source.indexOf("WorldCreationProgressReporter.start(sender, name)"); + int resolve = source.indexOf("0.02D, \"resolve_dimension\"", startReporter); + int validate = source.indexOf("0.06D, \"validate_pack\"", resolve); + int datapacks = source.indexOf("0.10D, \"install_datapacks\"", validate); + int pack = source.indexOf("0.26D, \"prepare_world_pack\"", datapacks); + int generator = source.indexOf("0.36D, \"prepare_generator\"", pack); + int createWorld = source.indexOf("0.44D, \"create_world\"", generator); + int register = source.indexOf("0.84D, \"register_world\"", createWorld); + int teleport = source.indexOf("0.92D, \"teleport_player\"", register); + int finalize = source.indexOf("0.99D, \"finalize\"", teleport); + int createReserved = source.indexOf("createReserved(worldKey, resolvedDimension, creationReporter)", validate); + int succeed = source.indexOf("creationReporter.succeed()", createReserved); + + assertTrue(startReporter >= 0); + assertTrue(resolve > startReporter); + assertTrue(validate > resolve); + assertTrue(datapacks > validate); + assertTrue(pack > datapacks); + assertTrue(generator > pack); + assertTrue(createWorld > generator); + assertTrue(register > createWorld); + assertTrue(teleport > register); + assertTrue(finalize > teleport); + assertTrue(createReserved > validate); + assertTrue(succeed > createReserved); + assertTrue(source.contains("creationReporter.fail()")); + assertTrue(source.contains("Form.f(generated) + \"/\" + Form.f(required) + \" chunks)\"")); + assertFalse(source.contains("RuntimeProgressMessages.WORLD_CREATE_ACTION")); + assertFalse(source.contains("RuntimeProgressMessages.WORLD_CREATE_CONSOLE")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/tools/WorldCreationProgressReporterTest.java b/core/src/test/java/art/arcane/iris/core/tools/WorldCreationProgressReporterTest.java new file mode 100644 index 000000000..2110c33a6 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/tools/WorldCreationProgressReporterTest.java @@ -0,0 +1,85 @@ +package art.arcane.iris.core.tools; + +import art.arcane.iris.core.localization.RuntimeProgressMessages; +import art.arcane.iris.util.common.format.C; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class WorldCreationProgressReporterTest { + @Test + public void playerBarAlwaysContainsFortyFourCellsAndClampsProgress() { + String half = WorldCreationProgressReporter.buildPlayerBar(0.5D); + String below = WorldCreationProgressReporter.buildPlayerBar(-1.0D); + String above = WorldCreationProgressReporter.buildPlayerBar(2.0D); + + assertEquals("[" + "|".repeat(44) + "]", C.stripColor(half)); + assertEquals(22, occurrences(half, C.GREEN.toString())); + assertEquals(0, occurrences(below, C.GREEN.toString())); + assertEquals(44, occurrences(above, C.GREEN.toString())); + } + + @Test + public void consoleBarIsReadableWithoutColorAndClampsProgress() { + assertEquals("[##########----------]", WorldCreationProgressReporter.buildConsoleBar(0.5D)); + assertEquals("[--------------------]", WorldCreationProgressReporter.buildConsoleBar(-1.0D)); + assertEquals("[####################]", WorldCreationProgressReporter.buildConsoleBar(2.0D)); + } + + @Test + public void everyCreationPhaseHasAStableLocalizedStage() { + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_RESOLVE_DIMENSION, + WorldCreationProgressReporter.stageKey("resolve_dimension")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_VALIDATE_PACK, + WorldCreationProgressReporter.stageKey("validate_pack")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_PREPARE_WORLD_PACK, + WorldCreationProgressReporter.stageKey("prepare_world_pack")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_INSTALL_DATAPACKS, + WorldCreationProgressReporter.stageKey("install_datapacks")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_PREPARE_GENERATOR, + WorldCreationProgressReporter.stageKey("prepare_generator")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_CREATE_WORLD, + WorldCreationProgressReporter.stageKey("create_world")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_REGISTER_WORLD, + WorldCreationProgressReporter.stageKey("register_world")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_TELEPORT_PLAYER, + WorldCreationProgressReporter.stageKey("teleport_player")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_PREGENERATE, + WorldCreationProgressReporter.stageKey("pregenerate")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_FINALIZE, + WorldCreationProgressReporter.stageKey("finalize")); + assertSame(RuntimeProgressMessages.WORLD_CREATE_STAGE_COMPLETE, + WorldCreationProgressReporter.stageKey("complete")); + } + + @Test + public void bukkitHudMutationIsMarshalledOffTheAsyncReporterThread() throws Exception { + String source = Files.readString(Path.of( + "src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java" + )); + + assertEquals(1, occurrences(source, "Bukkit.createBossBar(")); + assertTrue(source.contains("J.sfut(reporter::initializePlayerHud)")); + assertTrue(source.contains("J.runEntity(sender.player(), guardedRender)")); + assertTrue(source.contains("J.runEntity(sender.player(), render)")); + assertTrue(source.contains("J.runEntity(sender.player(), cleanup, 60, cleanup)")); + assertFalse(source.contains("HudSlotRequest")); + assertFalse(source.contains("hudLanes()")); + } + + private static int occurrences(String value, String match) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(match, offset)) >= 0) { + count++; + offset += match.length(); + } + return count; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java index 423cd9c7c..4170a13c7 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/DecoratorCoreTest.java @@ -149,6 +149,8 @@ public class DecoratorCoreTest { PlatformBlockState air = mock(PlatformBlockState.class); PlatformBlockState decorant = mock(PlatformBlockState.class); when(air.isAir()).thenReturn(true); + when(decorant.key()).thenReturn("minecraft:stone"); + when(decorant.canPlaceOnto(support)).thenReturn(true); when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(1); when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); @@ -198,6 +200,7 @@ public class DecoratorCoreTest { when(occupied.isAir()).thenReturn(false); when(occupied.isFluid()).thenReturn(false); when(decorant.key()).thenReturn("minecraft:tall_grass"); + when(decorant.canPlaceOnto(support)).thenReturn(true); when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(3); when(decorator.getTopThreshold()).thenReturn(0.75); when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); @@ -280,6 +283,7 @@ public class DecoratorCoreTest { IrisDecorator decorator = mock(IrisDecorator.class); IrisData data = mock(IrisData.class); PlatformBlockState decorant = mock(PlatformBlockState.class); + when(decorant.key()).thenReturn("minecraft:stone"); when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(3); when(decorator.getTopThreshold()).thenReturn(1.0); when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); @@ -345,6 +349,7 @@ public class DecoratorCoreTest { PlatformBlockState lower = mock(PlatformBlockState.class); PlatformBlockState upper = mock(PlatformBlockState.class); PlatformBlockState plant = tallPlantState(lower, upper); + when(plant.canPlaceOnto(support)).thenReturn(true); when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant); Hunk output = Hunk.newArrayHunk(1, 4, 1); @@ -359,6 +364,26 @@ public class DecoratorCoreTest { assertSame(upper, output.get(0, 2, 0)); } + @Test + public void surfaceDecorantRejectsInvalidNativeSupport() { + IrisDecorator decorator = mock(IrisDecorator.class); + IrisData data = mock(IrisData.class); + PlatformBlockState support = sturdyState(); + PlatformBlockState air = airState(); + PlatformBlockState decorant = mock(PlatformBlockState.class); + when(decorant.canPlaceOnto(support)).thenReturn(false); + when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); + + Hunk output = Hunk.newArrayHunk(1, 3, 1); + output.set(0, 0, 0, support); + output.set(0, 1, 0, air); + + DecoratorCore.placeSurfaceSingle( + decorator, 0, 0, 0, 0, 0, output, new RNG(1L), data, false, false, null); + + assertSame(air, output.get(0, 1, 0)); + } + @Test public void forcedSurfaceBlockStillPlacesDecorantAboveIt() { IrisDecorator decorator = mock(IrisDecorator.class); @@ -384,6 +409,26 @@ public class DecoratorCoreTest { assertSame(wheat, output.get(0, 1, 0)); } + @Test + public void descendingWeepingVinesUsePlantBodiesAndOneFreeEndTip() { + PlatformBlockState vine = mock(PlatformBlockState.class); + when(vine.key()).thenReturn("minecraft:weeping_vines"); + + assertEquals("minecraft:weeping_vines_plant", DecoratorCore.stackedVineKey(vine, 3, 0)); + assertEquals("minecraft:weeping_vines_plant", DecoratorCore.stackedVineKey(vine, 3, 1)); + assertEquals("minecraft:weeping_vines", DecoratorCore.stackedVineKey(vine, 3, 2)); + } + + @Test + public void ascendingTwistingVinesUsePlantBodiesAndOneFreeEndTip() { + PlatformBlockState vine = mock(PlatformBlockState.class); + when(vine.key()).thenReturn("minecraft:twisting_vines_plant"); + + assertEquals("minecraft:twisting_vines_plant", DecoratorCore.stackedVineKey(vine, 3, 0)); + assertEquals("minecraft:twisting_vines_plant", DecoratorCore.stackedVineKey(vine, 3, 1)); + assertEquals("minecraft:twisting_vines", DecoratorCore.stackedVineKey(vine, 3, 2)); + } + private PlatformBlockState airState() { PlatformBlockState air = mock(PlatformBlockState.class); when(air.isAir()).thenReturn(true); diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java index 9f78f224c..578f2eafb 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java @@ -65,6 +65,7 @@ public class IrisShoreLineDecoratorTest { Fixture fixture = createFixture(false); PlatformBlockState support = sturdyState(); PlatformBlockState targetAir = airState(); + when(fixture.decorant.canPlaceOnto(support)).thenReturn(true); Hunk output = output(support, targetAir); fixture.shoreline.decorate(0, 0, 0, 1, -1, 0, 1, -1, diff --git a/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStoreTest.java b/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStoreTest.java index 7e44b128e..3388001b1 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStoreTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/NativeStructureOwnershipStoreTest.java @@ -274,6 +274,27 @@ public class NativeStructureOwnershipStoreTest { assertEquals(2, storage.flushes); } + @Test + public void deferredFlushRemainsDirtyForSuccessfulRetryWithoutFailingAutosave() { + Engine engine = engine(); + DeferringFlushStorage storage = new DeferringFlushStorage(); + NativeStructureOwnershipStore.State state = + new NativeStructureOwnershipStore.State(engine, storage); + NativeStructureOwnershipRecord record = record( + "test:flush_deferred", 13, -16, 107L); + + state.record(record); + state.flush(); + state.flush(); + storage.crash(); + + NativeStructureOwnershipStore.State recovered = + new NativeStructureOwnershipStore.State(engine, storage); + assertEquals(record, recovered.findPersisted( + record.structureKey(), record.originChunkX(), record.originChunkZ())); + assertEquals(2, storage.flushes); + } + @Test public void cleanFlushPerformsNoStorageIo() { Engine engine = engine(); @@ -356,6 +377,29 @@ public class NativeStructureOwnershipStoreTest { assertEquals(2, storage.flushes); } + @Test + public void deferredCloseLeavesTheStoreOpenAndDirtyForRetry() { + Engine engine = engine(); + DeferringFlushStorage storage = new DeferringFlushStorage(); + NativeStructureOwnershipStore.State state = + new NativeStructureOwnershipStore.State(engine, storage); + NativeStructureOwnershipRecord record = record( + "test:close_deferred", -23, 24, 108L); + + state.record(record); + assertThrows(IllegalStateException.class, state::close); + assertEquals(record, state.findPersisted( + record.structureKey(), record.originChunkX(), record.originChunkZ())); + + state.close(); + storage.crash(); + NativeStructureOwnershipStore.State recovered = + new NativeStructureOwnershipStore.State(engine, storage); + assertEquals(record, recovered.findPersisted( + record.structureKey(), record.originChunkX(), record.originChunkZ())); + assertEquals(2, storage.flushes); + } + private static Engine engine() { Engine engine = mock(Engine.class); when(engine.isClosing()).thenReturn(false); @@ -434,8 +478,9 @@ public class NativeStructureOwnershipStoreTest { } @Override - public void flush() { + public boolean flush() { flushes++; + return true; } NativeStructureOwnershipRecord find(long target, @@ -450,10 +495,11 @@ public class NativeStructureOwnershipStoreTest { protected final Map durable = new ConcurrentHashMap<>(); @Override - public void flush() { + public boolean flush() { super.flush(); durable.clear(); durable.putAll(chunks); + return true; } void crash() { @@ -466,13 +512,29 @@ public class NativeStructureOwnershipStoreTest { private final AtomicBoolean fail = new AtomicBoolean(true); @Override - public void flush() { + public boolean flush() { flushes++; if (fail.compareAndSet(true, false)) { throw new IllegalStateException("Simulated ownership flush failure"); } durable.clear(); durable.putAll(chunks); + return true; + } + } + + private static final class DeferringFlushStorage extends CrashableStorage { + private final AtomicBoolean defer = new AtomicBoolean(true); + + @Override + public boolean flush() { + flushes++; + if (defer.compareAndSet(true, false)) { + return false; + } + durable.clear(); + durable.putAll(chunks); + return true; } } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentProceduralChanceTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentProceduralChanceTest.java new file mode 100644 index 000000000..d4bb8c405 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentProceduralChanceTest.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MantleObjectComponentProceduralChanceTest { + @Test + public void exactChanceBoundariesRemainExact() { + for (int seed = 0; seed < 1000; seed++) { + RNG rng = new RNG(seed); + assertFalse(MantleObjectComponent.passesProceduralChance(rng, 0.0)); + assertTrue(MantleObjectComponent.passesProceduralChance(rng, 1.0)); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java new file mode 100644 index 000000000..064eb0294 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java @@ -0,0 +1,104 @@ +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class IrisCarveModifierBoundarySupportTest { + @Test + @SuppressWarnings("unchecked") + public void boundaryBiomeUsesCustomMatterAtItsOwnY() { + IrisBiome customFloor = mock(IrisBiome.class); + IrisBiome resolvedCeiling = mock(IrisBiome.class); + IrisData data = mock(IrisData.class); + ResourceLoader biomeLoader = mock(ResourceLoader.class); + doReturn(biomeLoader).when(data).getBiomeLoader(); + doReturn(customFloor).when(biomeLoader).load("custom/floor"); + + Engine engine = mock(Engine.class); + doReturn(data).when(engine).getData(); + doReturn(resolvedCeiling).when(engine).getCaveBiome(anyInt(), eq(42), anyInt(), any(IrisDimensionCarvingResolver.State.class)); + + IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS); + doReturn(engine).when(modifier).getEngine(); + + MantleChunk mantleChunk = mock(MantleChunk.class); + doReturn(new MatterCavern(true, "custom/floor", (byte) 0)) + .when(mantleChunk).get(1, 6, 2, MatterCavern.class); + doReturn(null).when(mantleChunk).get(1, 42, 2, MatterCavern.class); + + Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(); + Map customBiomeCache = new HashMap<>(); + IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + + IrisBiome floor = modifier.resolveCaveBoundaryBiome( + mantleChunk, 1, 6, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); + IrisBiome ceiling = modifier.resolveCaveBoundaryBiome( + mantleChunk, 1, 42, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache); + + assertSame(customFloor, floor); + assertSame(resolvedCeiling, ceiling); + } + + @Test + @SuppressWarnings("unchecked") + public void gravityFloorLayerRequiresSolidSupportBelowItsTarget() { + Hunk output = mock(Hunk.class); + PlatformBlockState air = state("minecraft:cave_air", false); + PlatformBlockState solid = state("minecraft:stone", true); + PlatformBlockState sand = state("minecraft:sand", true); + PlatformBlockState stone = state("minecraft:stone", true); + + doReturn(air).when(output).getRaw(0, 4, 0); + assertFalse(IrisCarveModifier.canReplaceCaveFloorLayer(output, 0, 5, 0, sand)); + assertTrue(IrisCarveModifier.canReplaceCaveFloorLayer(output, 0, 5, 0, stone)); + + doReturn(solid).when(output).getRaw(0, 4, 0); + assertTrue(IrisCarveModifier.canReplaceCaveFloorLayer(output, 0, 5, 0, sand)); + } + + @Test + @SuppressWarnings("unchecked") + public void gravityFloorDoesNotReceiveDecoratorsOverLowerCaveAir() { + Hunk output = mock(Hunk.class); + PlatformBlockState air = state("minecraft:cave_air", false); + PlatformBlockState sand = state("minecraft:sand", true); + PlatformBlockState stone = state("minecraft:stone", true); + + doReturn(sand).when(output).getRaw(0, 5, 0); + doReturn(air).when(output).getRaw(0, 4, 0); + assertFalse(IrisCarveModifier.hasStableCaveFloorSupport(output, 0, 6, 0)); + + doReturn(stone).when(output).getRaw(0, 4, 0); + assertTrue(IrisCarveModifier.hasStableCaveFloorSupport(output, 0, 6, 0)); + } + + private PlatformBlockState state(String key, boolean solid) { + PlatformBlockState state = mock(PlatformBlockState.class); + doReturn(key).when(state).key(); + doReturn(solid).when(state).isSolid(); + return state; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java index 868786fe6..5111ffaa6 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java @@ -68,10 +68,12 @@ public class IrisCarveModifierInferenceIsolationTest { Map customBiomes = new HashMap<>(); customBiomes.put("shared", biome); + CarveWallBuffer walls = new CarveWallBuffer(1); + walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0)); Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod( "paintBoundaryZone", Hunk.class, - MatterCavern.class, + CarveWallBuffer.class, int.class, int.class, int.class, @@ -86,7 +88,7 @@ public class IrisCarveModifierInferenceIsolationTest { paintBoundaryZone.invoke( modifier, mock(Hunk.class), - new MatterCavern(true, "shared", (byte) 0), + walls, 0, 0, 0, diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java index e5df4bd10..d03e4cea1 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java @@ -30,6 +30,9 @@ public class IrisCarveScratchTest { buffer.put(5, 25, 15, replacement); expected.put(key(5, 25, 15), replacement); + assertSame(replacement, buffer.get(5, 25, 15)); + assertNull(buffer.get(5, 26, 15)); + Map actual = new HashMap<>(); buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern)); assertEquals(expected.keySet(), actual.keySet()); @@ -44,7 +47,6 @@ public class IrisCarveScratchTest { MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0); scratch.columnMasks[0].add(12); scratch.boundaryMasks[0].add(13); - scratch.boundaryCaverns[0] = cavern; scratch.walls.put(1, 12, 2, cavern); scratch.customBiomeCache.put("cave", null); scratch.customCaveBiomePresent = true; @@ -53,7 +55,6 @@ public class IrisCarveScratchTest { assertTrue(scratch.columnMasks[0].isEmpty()); assertTrue(scratch.boundaryMasks[0].isEmpty()); - assertNull(scratch.boundaryCaverns[0]); assertTrue(scratch.customBiomeCache.isEmpty()); assertFalse(scratch.customCaveBiomePresent); int[] wallCount = new int[1]; diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContainmentTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContainmentTest.java index bf1e3d37c..09ced48c1 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContainmentTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisDepositModifierContainmentTest.java @@ -18,7 +18,11 @@ package art.arcane.iris.engine.modifier; +import art.arcane.iris.engine.object.IrisDepositHeightDistribution; +import art.arcane.iris.engine.object.IrisDepositPlacementScope; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -32,6 +36,23 @@ public class IrisDepositModifierContainmentTest { public void depositSurfaceLimitKeepsEveryColumnSevenBlocksBuried() { assertEquals(73, IrisDepositModifier.depositSurfaceLimit(80)); assertEquals(24, IrisDepositModifier.depositSurfaceLimit(31)); + assertEquals(80, IrisDepositModifier.depositSurfaceLimit(80, 0)); + } + + @Test + public void placementScopeSeparatesTerrainFromFloatingSolids() { + assertTrue(IrisDepositModifier.placementSurfaceAllows( + IrisDepositPlacementScope.TERRAIN, 70, 80, 7)); + assertFalse(IrisDepositModifier.placementSurfaceAllows( + IrisDepositPlacementScope.TERRAIN, 74, 80, 7)); + assertFalse(IrisDepositModifier.placementSurfaceAllows( + IrisDepositPlacementScope.ABOVE_TERRAIN, 80, 80, 0)); + assertTrue(IrisDepositModifier.placementSurfaceAllows( + IrisDepositPlacementScope.ABOVE_TERRAIN, 81, 80, 0)); + assertTrue(IrisDepositModifier.placementSurfaceAllows( + IrisDepositPlacementScope.FULL_HEIGHT, 1, 80, 64)); + assertTrue(IrisDepositModifier.placementSurfaceAllows( + IrisDepositPlacementScope.FULL_HEIGHT, 700, 80, 64)); } @Test @@ -70,4 +91,85 @@ public class IrisDepositModifierContainmentTest { assertEquals(2, IrisDepositModifier.clampDepositCenter(1, 4, 16)); assertEquals(14, IrisDepositModifier.clampDepositCenter(15, 4, 16)); } + + @Test + public void vanillaHeightProvidersSampleAuthoredBandsBeforeWorldClipping() { + RNG uniform = new RNG(91L); + boolean sampledBelowBottom = false; + boolean sampledAboveBottom = false; + for (int i = 0; i < 2_000; i++) { + int y = IrisDepositModifier.sampleHeight( + IrisDepositHeightDistribution.UNIFORM, uniform, -80, 80, 20); + sampledBelowBottom |= y < 0; + sampledAboveBottom |= y > 20; + } + + assertTrue(sampledBelowBottom); + assertTrue(sampledAboveBottom); + assertEquals(14, IrisDepositModifier.sampleHeight( + IrisDepositHeightDistribution.UNIFORM, uniform, 14, 14, 10)); + } + + @Test + public void clippedHeightProviderPreservesLegacyTerrainClipping() { + RNG rng = new RNG(44L); + for (int i = 0; i < 2_000; i++) { + int y = IrisDepositModifier.sampleHeight( + IrisDepositHeightDistribution.CLIPPED_UNIFORM, rng, -80, 80, 20); + assertTrue(y >= 0); + assertTrue(y <= 20); + } + } + + @Test + public void triangleHeightProviderPeaksNearTheMidpoint() { + RNG rng = new RNG(112L); + int center = 0; + int edges = 0; + long total = 0L; + int samples = 100_000; + for (int i = 0; i < samples; i++) { + int y = IrisDepositModifier.sampleHeight( + IrisDepositHeightDistribution.TRIANGLE, rng, -32, 32, 32); + total += y; + if (Math.abs(y) <= 4) { + center++; + } + if (Math.abs(y) >= 28) { + edges++; + } + } + + assertTrue(Math.abs(total / (double) samples) < 0.25D); + assertTrue(center > edges * 4); + } + + @Test + public void exposureDiscardRequiresAdjacentAirAndPassingChanceRoll() { + assertFalse(IrisDepositModifier.shouldDiscardExposed(1D, 0D, false)); + assertTrue(IrisDepositModifier.shouldDiscardExposed(1D, 0.999D, true)); + assertTrue(IrisDepositModifier.shouldDiscardExposed(0.5D, 0.499D, true)); + assertFalse(IrisDepositModifier.shouldDiscardExposed(0.5D, 0.5D, true)); + assertFalse(IrisDepositModifier.shouldDiscardExposed(0D, 0D, true)); + } + + @Test + public void exposureProbeChecksOnlyInBoundsOrthogonalNeighbors() { + Hunk data = Hunk.newHunk(3, 3, 3); + PlatformBlockState solid = mock(PlatformBlockState.class); + PlatformBlockState air = mock(PlatformBlockState.class); + when(air.isAir()).thenReturn(true); + for (int x = 0; x < 3; x++) { + for (int y = 0; y < 3; y++) { + for (int z = 0; z < 3; z++) { + data.setRaw(x, y, z, solid); + } + } + } + + assertFalse(IrisDepositModifier.isAdjacentToAir(data, 1, 1, 1)); + data.setRaw(2, 1, 1, air); + assertTrue(IrisDepositModifier.isAdjacentToAir(data, 1, 1, 1)); + assertFalse(IrisDepositModifier.isAdjacentToAir(data, 0, 0, 0)); + } } diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java index cbe387229..a97e7938d 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeCeilingLayerGuardTest.java @@ -47,4 +47,9 @@ public class IrisBiomeCeilingLayerGuardTest { assertTrue("zeroed palettes with zero heights produce no blocks", result.isEmpty()); } + + @Test + public void omittedCeilingLayersLeaveCavesUnchanged() { + assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty()); + } } diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDecoratorHeightTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDecoratorHeightTest.java new file mode 100644 index 000000000..365949047 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDecoratorHeightTest.java @@ -0,0 +1,30 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisDecoratorHeightTest { + @Test + public void sampledHeightDoesNotExceedConfiguredMaximum() { + CNG generator = mock(CNG.class); + when(generator.fit(eq(1), eq(2), anyDouble(), anyDouble())).thenReturn(2); + IrisDecorator decorator = new IrisDecorator() { + @Override + public CNG getHeightGenerator(RNG rng, IrisData data) { + return generator; + } + }; + decorator.setStackMin(1); + decorator.setStackMax(2); + + assertEquals(2, decorator.getHeight(new RNG(1L), 0, 0, mock(IrisData.class))); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java index 7ca232a8b..6686f4ea5 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java @@ -2,6 +2,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.volmlib.util.math.RNG; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -52,6 +53,69 @@ public class IrisDepositTuningTest { assertFalse(stoneGenerator.isOre(data)); } + @Test + public void vanillaEllipsoidSizeControlsGeometryRatherThanExactBlockCount() { + IrisData data = mock(IrisData.class); + IrisDepositGenerator generator = generatorWithState(data, true); + long smallBlocks = 0L; + long mediumBlocks = 0L; + long largeBlocks = 0L; + int samples = 500; + for (int i = 0; i < samples; i++) { + smallBlocks += generator.generateVanillaEllipsoid(new RNG(i), data, 4).getBlocks().size(); + mediumBlocks += generator.generateVanillaEllipsoid(new RNG(i), data, 8).getBlocks().size(); + largeBlocks += generator.generateVanillaEllipsoid(new RNG(i), data, 17).getBlocks().size(); + } + + assertTrue(smallBlocks > 0L); + assertTrue(mediumBlocks > smallBlocks); + assertTrue(largeBlocks > mediumBlocks); + assertNotEquals(17L * samples, largeBlocks); + } + + @Test + public void vanillaScatteredSizeIsAnUpperCandidateBound() { + IrisData data = mock(IrisData.class); + IrisDepositGenerator generator = generatorWithState(data, true); + boolean foundEmpty = false; + boolean foundNonEmpty = false; + RNG rng = new RNG(991L); + for (int i = 0; i < 200; i++) { + int blocks = generator.generateVanillaScattered(rng, data, 3).getBlocks().size(); + assertTrue(blocks <= 3); + foundEmpty |= blocks == 0; + foundNonEmpty |= blocks > 0; + } + + assertTrue(foundEmpty); + assertTrue(foundNonEmpty); + } + + @Test + public void biomeFiltersAcceptResourceKeysAndVanillaDerivatives() { + IrisBiome mountain = new IrisBiome(); + mountain.setLoadKey("custom/mountain"); + mountain.setDerivative("minecraft:stony_peaks"); + IrisBiome plains = new IrisBiome(); + plains.setLoadKey("custom/plains"); + plains.setDerivative("minecraft:plains"); + IrisDepositGenerator generator = new IrisDepositGenerator(); + generator.setBiomeScope(IrisDepositBiomeScope.SURFACE); + generator.getIncludedBiomes().add("minecraft:stony_peaks"); + + assertTrue(generator.matchesBiome(mountain, plains)); + assertFalse(generator.matchesBiome(plains, mountain)); + assertFalse(generator.usesCaveBiomeFilter()); + + generator.getIncludedBiomes().clear(); + generator.getExcludedBiomes().add("custom/mountain"); + assertFalse(generator.matchesBiome(mountain, plains)); + assertTrue(generator.matchesBiome(plains, mountain)); + + generator.setBiomeScope(IrisDepositBiomeScope.CAVE); + assertTrue(generator.usesCaveBiomeFilter()); + } + private IrisDepositGenerator generatorWithState(IrisData data, boolean ore) { IrisBlockData block = mock(IrisBlockData.class); PlatformBlockState state = mock(PlatformBlockState.class); diff --git a/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java b/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java index e99ebda6c..f3f94280b 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java @@ -90,6 +90,17 @@ public class NativeStructureObjectVetoTest { assertTrue(placer.written().isEmpty()); } + @Test + public void negativeWorldMinimumConvertsMantleCoordinatesBeforeVeto() { + when(engine.getMinHeight()).thenReturn(-256); + int worldBaseY = plantedBottomY() - 256; + RecordingPlacer placer = new RecordingPlacer(engine); + volumes(volume(0, worldBaseY, 0, 0, worldBaseY, 0)); + + assertEquals(-1, place(placer)); + assertTrue(placer.written().isEmpty()); + } + @Test public void pieceOverlappingOnlyTheEnvelopeStillPlaces() { RecordingPlacer placer = new RecordingPlacer(engine); diff --git a/core/src/test/java/art/arcane/iris/engine/object/formation/FormationShapeBuilderTest.java b/core/src/test/java/art/arcane/iris/engine/object/formation/FormationShapeBuilderTest.java new file mode 100644 index 000000000..da69c1ac1 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/formation/FormationShapeBuilderTest.java @@ -0,0 +1,352 @@ +package art.arcane.iris.engine.object.formation; + +import art.arcane.iris.engine.object.IrisFormation; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class FormationShapeBuilderTest { + @Test + public void magicalFormsAreDeterministicAndGrounded() { + assertDeterministicAndGrounded(this::iceberg); + assertDeterministicAndGrounded(this::fissure); + assertDeterministicAndGrounded(this::spiral); + assertDeterministicAndGrounded(this::overhang); + } + + @Test + public void icebergBuildsWideBodyWithMultipleSummits() { + FormationCanvas canvas = iceberg(41L); + + assertTrue(maxY(canvas) >= 12); + assertTrue(horizontalDiameter(canvas) >= 12); + assertTrue(localSummits(canvas) >= 2); + } + + @Test + public void fissureKeepsShardsSeparatedByOpenCracks() { + FormationCanvas canvas = fissure(73L); + + assertTrue(connectedComponents(canvas) >= 3); + assertTrue(maxY(canvas) >= 10); + } + + @Test + public void spiralLeavesAnOpenCenterAndCirclesMultipleQuadrants() { + FormationCanvas canvas = spiral(101L); + int middleY = 9; + + assertFalse(canvas.has(0, middleY, 0)); + assertTrue(hasQuadrant(canvas, true, true)); + assertTrue(hasQuadrant(canvas, true, false)); + assertTrue(hasQuadrant(canvas, false, true)); + assertTrue(hasQuadrant(canvas, false, false)); + } + + @Test + public void overhangCantileversBeyondItsGroundedFoot() { + FormationCanvas canvas = overhang(131L); + double footReach = maxHorizontalReach(canvas, 0, 2); + double upperReach = maxHorizontalReach(canvas, 9, Integer.MAX_VALUE); + + assertTrue(upperReach >= footReach + 5.0); + } + + @Test + public void thinLeaningSpireRetainsAConnectedPointedTip() { + IrisFormation formation = baseFormation(); + formation.setRoughness(0.12); + formation.setJitter(0.02); + formation.setTopWidth(0); + formation.setLean(10); + formation.setLeanAzimuth(18); + FormationCanvas canvas = new FormationCanvas(); + int height = 48; + FormationShapeBuilder.spire(canvas, formation, height, 1.0, new RNG(137L)); + double shear = Math.tan(Math.toRadians(formation.getLean())) * (height - 1); + int tipX = (int) Math.round(Math.cos(Math.toRadians(formation.getLeanAzimuth())) * shear); + int tipZ = (int) Math.round(Math.sin(Math.toRadians(formation.getLeanAzimuth())) * shear); + + assertEquals(FormationCanvas.Role.CAP, canvas.getCells().get(new Vector3i(tipX, height - 1, tipZ))); + assertEquals(height - 1, maxY(canvas)); + assertEquals(1, connectedComponents(canvas)); + assertTrue(maxLayerDiameter(canvas) <= 3); + } + + @Test + public void archRetainsGroundedLegsAndAnOpenCenter() { + IrisFormation formation = baseFormation(); + formation.setArchSpan(10); + formation.setArchThickness(2); + formation.setArchAsymmetry(0.8); + FormationCanvas canvas = arch(formation, 149L); + FormationCanvas repeated = arch(formation, 149L); + FormationCanvas different = arch(formation, 151L); + + assertEquals(canvas.getCells(), repeated.getCells()); + assertFalse(canvas.getCells().equals(different.getCells())); + assertEquals(1, connectedComponents(canvas)); + assertTrue(layerComponents(canvas, 0) >= 2); + assertTrue(openCenterColumn(canvas, 3, 6)); + assertTrue(axisDiameter(canvas, false) >= 4); + assertTrue(maxY(canvas) >= 16); + assertTrue(maxY(canvas) <= 18); + assertTrue(unmirroredCells(canvas) >= 20); + } + + @Test + public void zeroAsymmetryKeepsArchMirroredAndThicknessScales() { + IrisFormation formation = baseFormation(); + formation.setRoughness(0.0); + formation.setJitter(0.0); + formation.setArchSpan(10); + formation.setArchAsymmetry(0.0); + + formation.setArchThickness(1); + FormationCanvas thin = arch(formation, 173L); + formation.setArchThickness(2); + FormationCanvas medium = arch(formation, 173L); + formation.setArchThickness(3); + FormationCanvas thick = arch(formation, 173L); + + assertEquals(0, unmirroredCells(medium)); + assertTrue(thin.getCells().size() < medium.getCells().size()); + assertTrue(medium.getCells().size() < thick.getCells().size()); + } + + private void assertDeterministicAndGrounded(CanvasFactory factory) { + FormationCanvas first = factory.create(31L); + FormationCanvas second = factory.create(31L); + + assertFalse(first.isEmpty()); + assertEquals(first.getCells(), second.getCells()); + assertTrue(hasLayer(first, 0)); + } + + private FormationCanvas iceberg(long seed) { + IrisFormation formation = baseFormation(); + formation.setIcebergPeaks(4); + FormationCanvas canvas = new FormationCanvas(); + FormationShapeBuilder.iceberg(canvas, formation, 20, 5.0, new RNG(seed)); + return canvas; + } + + private FormationCanvas fissure(long seed) { + IrisFormation formation = baseFormation(); + formation.setFractureCount(3); + formation.setFractureSeparation(4); + FormationCanvas canvas = new FormationCanvas(); + FormationShapeBuilder.fissure(canvas, formation, 18, 4.0, new RNG(seed)); + return canvas; + } + + private FormationCanvas spiral(long seed) { + IrisFormation formation = baseFormation(); + formation.setSpiralTurns(1.75); + formation.setSpiralRadius(6); + formation.setSpiralThickness(1); + FormationCanvas canvas = new FormationCanvas(); + FormationShapeBuilder.spiral(canvas, formation, 20, 4.0, new RNG(seed)); + return canvas; + } + + private FormationCanvas overhang(long seed) { + IrisFormation formation = baseFormation(); + formation.setOverhangReach(14); + formation.setOverhangDrop(4); + FormationCanvas canvas = new FormationCanvas(); + FormationShapeBuilder.overhang(canvas, formation, 20, 3.0, new RNG(seed)); + return canvas; + } + + private FormationCanvas arch(IrisFormation formation, long seed) { + FormationCanvas canvas = new FormationCanvas(); + FormationShapeBuilder.arch(canvas, formation, 18, 4.0, new RNG(seed)); + return canvas; + } + + private IrisFormation baseFormation() { + IrisFormation formation = new IrisFormation(); + formation.setRoughness(0.2); + formation.setJitter(0.05); + return formation; + } + + private boolean hasLayer(FormationCanvas canvas, int y) { + for (Vector3i position : canvas.getCells().keySet()) { + if (position.getBlockY() == y) { + return true; + } + } + return false; + } + + private int maxY(FormationCanvas canvas) { + int max = Integer.MIN_VALUE; + for (Vector3i position : canvas.getCells().keySet()) { + max = Math.max(max, position.getBlockY()); + } + return max; + } + + private int horizontalDiameter(FormationCanvas canvas) { + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minZ = Integer.MAX_VALUE; + int maxZ = Integer.MIN_VALUE; + for (Vector3i position : canvas.getCells().keySet()) { + minX = Math.min(minX, position.getBlockX()); + maxX = Math.max(maxX, position.getBlockX()); + minZ = Math.min(minZ, position.getBlockZ()); + maxZ = Math.max(maxZ, position.getBlockZ()); + } + return Math.max(maxX - minX + 1, maxZ - minZ + 1); + } + + private int axisDiameter(FormationCanvas canvas, boolean xAxis) { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + for (Vector3i position : canvas.getCells().keySet()) { + int coordinate = xAxis ? position.getBlockX() : position.getBlockZ(); + min = Math.min(min, coordinate); + max = Math.max(max, coordinate); + } + return max - min + 1; + } + + private int maxLayerDiameter(FormationCanvas canvas) { + int maximum = 0; + for (int y = 0; y <= maxY(canvas); y++) { + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minZ = Integer.MAX_VALUE; + int maxZ = Integer.MIN_VALUE; + for (Vector3i position : canvas.getCells().keySet()) { + if (position.getBlockY() != y) { + continue; + } + minX = Math.min(minX, position.getBlockX()); + maxX = Math.max(maxX, position.getBlockX()); + minZ = Math.min(minZ, position.getBlockZ()); + maxZ = Math.max(maxZ, position.getBlockZ()); + } + if (minX != Integer.MAX_VALUE) { + maximum = Math.max(maximum, Math.max(maxX - minX + 1, maxZ - minZ + 1)); + } + } + return maximum; + } + + private boolean openCenterColumn(FormationCanvas canvas, int halfWidth, int maxY) { + for (int x = -halfWidth; x <= halfWidth; x++) { + for (int y = 0; y <= maxY; y++) { + if (canvas.has(x, y, 0)) { + return false; + } + } + } + return true; + } + + private int unmirroredCells(FormationCanvas canvas) { + int unmatched = 0; + for (Vector3i position : canvas.getCells().keySet()) { + if (!canvas.has(-position.getBlockX(), position.getBlockY(), position.getBlockZ())) { + unmatched++; + } + } + return unmatched; + } + + private int localSummits(FormationCanvas canvas) { + Set summits = new HashSet<>(); + for (Vector3i position : canvas.getCells().keySet()) { + if (!canvas.has(position.getBlockX(), position.getBlockY() + 1, position.getBlockZ())) { + int bucketX = Math.floorDiv(position.getBlockX(), 3); + int bucketZ = Math.floorDiv(position.getBlockZ(), 3); + if (position.getBlockY() >= 8) { + summits.add(bucketX + ":" + bucketZ); + } + } + } + return summits.size(); + } + + private int connectedComponents(FormationCanvas canvas) { + Set remaining = new HashSet<>(canvas.getCells().keySet()); + int components = 0; + while (!remaining.isEmpty()) { + Vector3i start = remaining.iterator().next(); + remaining.remove(start); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(start); + while (!queue.isEmpty()) { + Vector3i current = queue.removeFirst(); + for (Vector3i neighbor : neighbors(current)) { + if (remaining.remove(neighbor)) { + queue.addLast(neighbor); + } + } + } + components++; + } + return components; + } + + private int layerComponents(FormationCanvas canvas, int y) { + FormationCanvas layer = new FormationCanvas(); + for (Map.Entry entry : canvas.getCells().entrySet()) { + Vector3i position = entry.getKey(); + if (position.getBlockY() == y) { + layer.setBody(position.getBlockX(), position.getBlockY(), position.getBlockZ()); + } + } + return connectedComponents(layer); + } + + private Set neighbors(Vector3i position) { + Set neighbors = new HashSet<>(); + neighbors.add(new Vector3i(position.getBlockX() + 1, position.getBlockY(), position.getBlockZ())); + neighbors.add(new Vector3i(position.getBlockX() - 1, position.getBlockY(), position.getBlockZ())); + neighbors.add(new Vector3i(position.getBlockX(), position.getBlockY() + 1, position.getBlockZ())); + neighbors.add(new Vector3i(position.getBlockX(), position.getBlockY() - 1, position.getBlockZ())); + neighbors.add(new Vector3i(position.getBlockX(), position.getBlockY(), position.getBlockZ() + 1)); + neighbors.add(new Vector3i(position.getBlockX(), position.getBlockY(), position.getBlockZ() - 1)); + return neighbors; + } + + private boolean hasQuadrant(FormationCanvas canvas, boolean positiveX, boolean positiveZ) { + for (Vector3i position : canvas.getCells().keySet()) { + boolean matchesX = positiveX ? position.getBlockX() >= 3 : position.getBlockX() <= -3; + boolean matchesZ = positiveZ ? position.getBlockZ() >= 3 : position.getBlockZ() <= -3; + if (matchesX && matchesZ) { + return true; + } + } + return false; + } + + private double maxHorizontalReach(FormationCanvas canvas, int minY, int maxY) { + double max = 0.0; + for (Vector3i position : canvas.getCells().keySet()) { + if (position.getBlockY() < minY || position.getBlockY() > maxY) { + continue; + } + max = Math.max(max, Math.hypot(position.getBlockX(), position.getBlockZ())); + } + return max; + } + + private interface CanvasFactory { + FormationCanvas create(long seed); + } +}