This commit is contained in:
Brian Neumann-Fopiano
2026-08-16 23:53:42 -04:00
parent a1cef36f10
commit 03746468ec
86 changed files with 3681 additions and 833 deletions
@@ -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<BlockPos, BlockState> 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<BlockPos, BlockState> 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;
}
@@ -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) {
@@ -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;
}
@@ -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) {
@@ -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;
}
}
@@ -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<String> WARNED_SOURCE_BUDGET =
ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_VACUUM_BUDGET =
ConcurrentHashMap.newKeySet();
@@ -45,23 +47,24 @@ public final class NativeStructureSurfaceFitter {
List<NativeStructureTerrainIntegrator.TerrainTarget> 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<NativeStructureTerrainIntegrator.TerrainTarget> 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<SurfaceAnchor> anchors = collectSourceSurfaceAnchors(targets);
List<SurfaceAnchor> 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<SurfaceAnchor> collectSourceSurfaceAnchors(
List<NativeStructureTerrainIntegrator.TerrainTarget> targets) {
WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> 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<SurfaceAnchor> 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<SurfaceAnchor> 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<SurfaceAnchor> 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<Long, NativeStructureTemplateOccupancy.LowestCell> 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<NativeStructureTerrainIntegrator.TerrainTarget> targets,
@@ -286,7 +353,7 @@ public final class NativeStructureSurfaceFitter {
Map<Long, Integer> targetCaps = new HashMap<>();
Set<Long> targetFoundationBases = new HashSet<>();
Set<Long> 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);
}
}
@@ -89,6 +89,24 @@ final class NativeStructureTemplateOccupancy {
return lowest;
}
static Map<Long, LowestCell> lowestProcessedSolidCells(Map<Long, OccupancyCell> occupancy) {
Map<Long, LowestCell> lowest = new HashMap<>();
for (Map.Entry<Long, OccupancyCell> 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)
@@ -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)) {
@@ -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()));
}
}
+2
View File
@@ -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
@@ -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<String> 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<String, String> computeRegistryRequirements(
List<File> packRoots,
IDataFixer fixer
) throws IOException {
Objects.requireNonNull(packRoots, "packRoots");
Objects.requireNonNull(fixer, "fixer");
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Set<String> installedBiomeKeys = new LinkedHashSet<>();
for (File packRoot : packRoots) {
PackDirectoryResolver.requireSafePackTree(packRoot);
if (!hasDimensions(packRoot.toPath())) {
continue;
}
IrisData data = IrisData.openDatapackCompiler(packRoot);
try {
ResourceLoader<IrisDimension> 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<String, String> computeRegistryRequirements(
IrisDimension dimension,
IDataFixer fixer
) throws IOException {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
collectRegistryRequirements(
requirements,
new LinkedHashSet<>(),
requiredDimension,
requiredFixer);
return Map.copyOf(requirements);
}
private static void collectRegistryRequirements(
Map<String, String> requirements,
Set<String> 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<String> 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(
@@ -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<String, String> 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<String, String> 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<String, String> loadedRequirements,
Map<String, String> requiredEntries
) {
if (loadedRequirements == null || requiredEntries == null || requiredEntries.isEmpty()) {
return false;
}
for (Map.Entry<String, String> 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()
@@ -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,
@@ -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,
@@ -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,
@@ -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<String> blockingErrors) {
static List<String> validateDecoratorPalettes(File biomesFolder, File decoratorSnippetsFolder) {
List<String> blockingErrors = new ArrayList<>();
if (biomesFolder != null && biomesFolder.isDirectory()) {
List<File> 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<File> 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<String> 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<String> 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<String> blockingErrors) {
if (!biome.has(field) || biome.isNull(field)) {
return DEFAULT_LAYER_COUNT;
return defaultCount;
}
JSONArray array = biome.optJSONArray(field);
@@ -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());
@@ -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);
}
} 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);
}
} else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1)))));
}
@@ -162,9 +122,6 @@ 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)),
@@ -172,15 +129,6 @@ final class StudioOpenProgressReporter {
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
));
} else if (loaderSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:studio-open", IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_PROGRESS,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0))
), currentProgress, BarColor.GREEN, BarStyle.SOLID, 4000L);
}
} else {
long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get();
@@ -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,10 +142,7 @@ 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");
if (sender.isPlayer()) {
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.CHUNK_ACTION_PROGRESS,
MessageArgument.trusted("bar", progressBar(currentProgress)),
@@ -169,16 +151,6 @@ public final class ChunkJobReporter {
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);
}
}
}, 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");
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)
));
} 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);
}
}
sender.sendMessage(IrisLanguage.text(
ok ? RuntimeProgressMessages.CHUNK_COMPLETE : RuntimeProgressMessages.CHUNK_FAILED,
@@ -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<Void> 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",
@@ -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,21 +237,15 @@ 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,
@@ -273,9 +255,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
BarStyle.SEGMENTED_20,
1_500L
);
} else {
BukkitPlatform.hudLanes().hide(player, hudLaneId);
}
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,15 +306,10 @@ 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,
@@ -344,7 +319,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
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() {
@@ -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) {
@@ -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");
}
}
@@ -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<String, Long> 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("|");
if (creationReporter != null) {
creationReporter.update(
0.44D + (0.38D * progress),
"create_world",
C.DARK_GRAY + " (" + Form.f(generated) + "/" + Form.f(required) + " chunks)"
);
}
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;
}
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,10 +612,16 @@ 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");
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);
@@ -614,13 +635,6 @@ public class IrisCreator {
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);
}
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) {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> stage;
private final AtomicReference<String> detail;
private final AtomicReference<Double> 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;
};
}
}
@@ -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;
};
}
}
@@ -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);
}
}
@@ -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 + "<font:minecraft:uniform>" + IrisLanguage.text(RuntimeUiMessages.ENGINE_HOTLOADED), 70, 60, 410);
s.sendAction(C.IRIS + IrisLanguage.text(RuntimeUiMessages.ENGINE_HOTLOADED));
}
});
}
@@ -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();
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<Long, NativeStructureOwnershipBundle> pending = new TreeMap<>(pendingBundles);
if (pending.isEmpty()) {
return;
return true;
}
Mantle<Matter> mantle = mantle();
@@ -309,11 +315,16 @@ public final class NativeStructureOwnershipStore {
restorePendingBundle(mantle, target, entry.getValue());
regions.add(Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5));
}
mantle.saveTectonicPlates(regions);
Set<Long> deferredRegions = mantle.saveIdleTectonicPlates(regions);
for (Map.Entry<Long, NativeStructureOwnershipBundle> 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<Matter> mantle, long target,
NativeStructureOwnershipBundle bundle) {
@@ -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,
@@ -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<PlatformBlockState
CarveWallBuffer walls = scratch.walls;
CarveColumnMask[] columnMasks = scratch.columnMasks;
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
MatterCavern[] boundaryCaverns = scratch.boundaryCaverns;
int[] surfaceHeights = scratch.surfaceHeights;
Map<String, IrisBiome> customBiomeCache = scratch.customBiomeCache;
UpperDimensionContext upperCtx = getEngine().getUpperContext();
@@ -155,7 +155,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} else {
addInternalWallsFromMasks(walls, columnMasks);
}
addCrossChunkBoundaryWalls(mantle, mantleChunk, walls, boundaryMasks, boundaryCaverns, x, z, surfaceHeights);
addCrossChunkBoundaryWalls(mantle, mantleChunk, walls, boundaryMasks, x, z, surfaceHeights);
getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
@@ -179,18 +179,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
});
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache);
processColumnFromMask(output, mantleChunk, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
}
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
continue;
}
MatterCavern cavern = boundaryCaverns[columnIndex];
if (cavern == null) {
continue;
}
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], cavern, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
}
// Surface-break carving must not leave an ore cap suspended across the opening.
@@ -320,7 +316,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
MantleChunk<Matter> mc,
CarveWallBuffer walls,
CarveColumnMask[] boundaryMasks,
MatterCavern[] boundaryCaverns,
int chunkX,
int chunkZ,
int[] surfaceHeights
@@ -349,16 +344,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
for (int yy = 1; yy <= maxY; yy++) {
for (int offset = 0; offset < 16; offset++) {
if (west != null) {
tryAddBoundaryWall(mc, west, walls, boundaryMasks, boundaryCaverns, 0, yy, offset, 15, offset);
tryAddBoundaryWall(mc, west, walls, boundaryMasks, 0, yy, offset, 15, offset);
}
if (east != null) {
tryAddBoundaryWall(mc, east, walls, boundaryMasks, boundaryCaverns, 15, yy, offset, 0, offset);
tryAddBoundaryWall(mc, east, walls, boundaryMasks, 15, yy, offset, 0, offset);
}
if (north != null) {
tryAddBoundaryWall(mc, north, walls, boundaryMasks, boundaryCaverns, offset, yy, 0, offset, 15);
tryAddBoundaryWall(mc, north, walls, boundaryMasks, offset, yy, 0, offset, 15);
}
if (south != null) {
tryAddBoundaryWall(mc, south, walls, boundaryMasks, boundaryCaverns, offset, yy, 15, offset, 0);
tryAddBoundaryWall(mc, south, walls, boundaryMasks, offset, yy, 15, offset, 0);
}
}
}
@@ -369,7 +364,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
MantleChunk<Matter> neighborChunk,
CarveWallBuffer walls,
CarveColumnMask[] boundaryMasks,
MatterCavern[] boundaryCaverns,
int localX,
int yy,
int localZ,
@@ -388,9 +382,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
walls.put(localX, yy, localZ, neighbor);
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
boundaryMasks[columnIndex].add(yy);
if (boundaryCaverns[columnIndex] == null) {
boundaryCaverns[columnIndex] = neighbor;
}
}
private MantleChunk<Matter> existingMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
@@ -410,7 +401,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int chunkX,
int chunkZ,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache
) {
if (columnMask == null || columnMask.isEmpty()) {
return;
@@ -437,7 +429,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
zone.ceiling = buf;
} else {
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache);
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
zone = new CaveZone();
zone.setFloor(y);
@@ -449,14 +441,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
if (zone.isValid(getEngine())) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache);
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
}
private void processBoundaryColumnFromMask(
Hunk<PlatformBlockState> output,
CarveColumnMask boundaryMask,
MatterCavern cavern,
CarveWallBuffer walls,
int columnIndex,
int chunkX,
int chunkZ,
@@ -481,19 +473,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (y == zoneCeiling + 1) {
zoneCeiling = y;
} else {
paintBoundaryZone(output, cavern, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
zoneFloor = y;
zoneCeiling = y;
}
y = boundaryMask.nextSetBit(y + 1);
}
paintBoundaryZone(output, cavern, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
paintBoundaryZone(output, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
}
private void paintBoundaryZone(
Hunk<PlatformBlockState> output,
MatterCavern cavern,
CarveWallBuffer walls,
int rx,
int rz,
int worldX,
@@ -504,62 +496,60 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> 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<PlatformBlockState> floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
if (floorBiome != null) {
KList<PlatformBlockState> 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 fy = zoneFloor - i - 1;
if (fy < 0) {
int floorY = zoneFloor - i - 1;
if (floorY < 0) {
break;
}
PlatformBlockState down = output.getRaw(rx, fy, rz);
if (!B.isSolid(down)) {
break;
}
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
PlatformBlockState layer = floorLayers.get(i);
if (B.isOre(down)) {
output.setRaw(rx, fy, rz, B.toDeepSlateOre(down, layer));
if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
continue;
}
output.setRaw(rx, fy, rz, layer);
if (B.isOre(existing)) {
output.setRaw(rx, floorY, rz, B.toDeepSlateOre(existing, layer));
continue;
}
output.setRaw(rx, floorY, rz, layer);
}
}
if (ceilingBiome != null) {
int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
KList<PlatformBlockState> ceilingLayers = biome.generateCeilingLayers(getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex());
KList<PlatformBlockState> ceilingLayers = ceilingBiome.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) {
int ceilingY = zoneCeiling + i + 1;
if (ceilingY >= worldMaxY) {
break;
}
PlatformBlockState up = output.getRaw(rx, cy, rz);
if (!B.isSolid(up)) {
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
if (!B.isSolid(existing)) {
continue;
}
PlatformBlockState layer = ceilingLayers.get(i);
if (B.isOre(up)) {
output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, layer));
if (B.isOre(existing)) {
output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer));
continue;
}
output.setRaw(rx, cy, rz, layer);
output.setRaw(rx, ceilingY, rz, layer);
}
}
}
@@ -575,10 +565,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return (h & 15L) == 0L;
}
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
int center = (zone.floor + zone.ceiling) / 2;
private void processZone(Hunk<PlatformBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> 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<PlatformBlockState
mantle.set(xx, zone.floor, zz, MarkerMatter.CAVE_FLOOR);
}
for (int i = zone.floor; i <= zone.ceiling; i++) {
MatterCavern cavernData = (MatterCavern) mc.getOrCreate(PowerOfTwoCoordinates.floorDivPow2(i, 4)).slice(MatterCavern.class)
.get(rx, i & 15, rz);
if (cavernData != null && !cavernData.getCustomBiome().isEmpty()) {
customBiome = cavernData.getCustomBiome();
break;
}
}
IrisBiome biome = customBiome.isEmpty()
? resolveCaveBiome(caveBiomeCache, xx, center, zz, resolverState)
: getEngine().getData().getBiomeLoader().load(customBiome);
if (biome == null) {
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
return;
}
KList<PlatformBlockState> blocks = biome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
if (floorBiome != null) {
KList<PlatformBlockState> floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
for (int i = 0; i < zone.floor - 1; i++) {
if (!blocks.hasIndex(i)) {
if (!floorBlocks.hasIndex(i)) {
break;
}
int y = zone.floor - i - 1;
PlatformBlockState b = blocks.get(i);
PlatformBlockState down = output.getRaw(rx, y, rz);
if (!B.isSolid(down)) {
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(down)) {
output.setRaw(rx, y, rz, B.toDeepSlateOre(down, b));
if (B.isOre(existing)) {
output.setRaw(rx, y, rz, B.toDeepSlateOre(existing, block));
continue;
}
output.setRaw(rx, y, rz, blocks.get(i));
output.setRaw(rx, y, rz, block);
}
}
blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex());
for (int i = 0; i < blocks.size(); i++) {
if (ceilingBiome != null) {
KList<PlatformBlockState> 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 b = blocks.get(i);
PlatformBlockState up = output.getRaw(rx, cy, rz);
if (!B.isSolid(up)) {
PlatformBlockState block = ceilingBlocks.get(i);
PlatformBlockState existing = output.getRaw(rx, cy, rz);
if (!B.isSolid(existing)) {
continue;
}
if (B.isOre(up)) {
output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, b));
if (B.isOre(existing)) {
output.setRaw(rx, cy, rz, B.toDeepSlateOre(existing, block));
continue;
}
output.setRaw(rx, cy, rz, b);
output.setRaw(rx, cy, rz, block);
}
}
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<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> 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<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
if (cavern != null && !cavern.getCustomBiome().isEmpty()) {
return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome());
}
return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState);
}
static boolean canReplaceCaveFloorLayer(Hunk<PlatformBlockState> 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<PlatformBlockState> 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<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState);
if (center == null) {
@@ -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<String, IrisBiome> 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];
@@ -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<PlatformBlockSta
int x = rng.i(min, max + 1);
int z = rng.i(min, max + 1);
int height = getDepositSurfaceLimit(cx, cz, x, z, he, context);
int terrainSurface = getDepositTerrainSurface(cx, cz, x, z, he, context);
int height = k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN
? depositSurfaceLimit(terrainSurface, k.getSurfaceClearance())
: getEngine().getHeight() - 1;
if (height <= 0)
if (height < 0) {
continue;
}
int y = sampleHeight(
k.getHeightDistribution(), rng, k.getMinHeight(), k.getMaxHeight(),
Math.min(height, getEngine().getHeight() - 1));
if (y == Integer.MIN_VALUE) {
continue;
}
boolean clippedHeight = k.getHeightDistribution() == IrisDepositHeightDistribution.CLIPPED_UNIFORM;
if (clippedHeight && y > 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<PlatformBlockSta
IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier);
int scaledDimension = scaledClump.getW();
x = clampDepositCenter(x, scaledDimension, 16);
if (clippedHeight) {
y = clampDepositCenter(y, scaledDimension, getEngine().getHeight());
}
z = clampDepositCenter(z, scaledDimension, 16);
clump = scaledClump;
}
@@ -163,8 +179,9 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (nx > 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<PlatformBlockSta
if (!canReplaceDepositTarget(current)) {
continue;
}
if (!k.canReplace(current)) {
continue;
}
if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) {
continue;
}
if (shouldDiscardExposed(
k.getDiscardChanceOnAirExposure(), rng.d(), isAdjacentToAir(data, nx, ny, nz))) {
continue;
}
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
PlatformBlockState ore = clump.getBlocks().get(j);
@@ -189,15 +213,30 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
}
private int getDepositSurfaceLimit(int cx, int cz, int localX, int localZ, HeightMap heightMap, ChunkContext context) {
int surfaceY = heightMap != null
private int getDepositTerrainSurface(
int cx, int cz, int localX, int localZ, HeightMap heightMap,
ChunkContext context) {
return heightMap != null
? heightMap.getHeight((cx << 4) + localX, (cz << 4) + localZ)
: context.getRoundedHeight(localX, localZ);
return depositSurfaceLimit(surfaceY);
}
static int depositSurfaceLimit(int surfaceY) {
return surfaceY - 7;
return depositSurfaceLimit(surfaceY, 7);
}
static int depositSurfaceLimit(int surfaceY, int surfaceClearance) {
return surfaceY - Math.max(0, surfaceClearance);
}
static boolean placementSurfaceAllows(
IrisDepositPlacementScope scope, int candidateY, int surfaceY, int surfaceClearance) {
int clearance = Math.max(0, surfaceClearance);
return switch (scope) {
case ABOVE_TERRAIN -> 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<PlatformBlockSta
return state != null && !state.isAir() && !state.isFluid();
}
static int sampleHeight(
IrisDepositHeightDistribution distribution, RNG rng,
int configuredMinimum, int configuredMaximum, int clippedMaximum) {
int minimum = distribution == IrisDepositHeightDistribution.CLIPPED_UNIFORM
? Math.max(0, configuredMinimum)
: configuredMinimum;
int maximum = distribution == IrisDepositHeightDistribution.CLIPPED_UNIFORM
? Math.min(clippedMaximum, configuredMaximum)
: configuredMaximum;
if (minimum > 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<PlatformBlockState> 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<PlatformBlockState> 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);
}
@@ -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<IrisBiomePaletteLayer> layers = new KList<IrisBiomePaletteLayer>().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<IrisBiomePaletteLayer> caveCeilingLayers = new KList<IrisBiomePaletteLayer>().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<IrisBiomePaletteLayer> 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<IrisBiomePaletteLayer> seaLayers = new KList<>();
@@ -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) {
@@ -0,0 +1,6 @@
package art.arcane.iris.engine.object;
public enum IrisDepositBiomeScope {
SURFACE,
CAVE
}
@@ -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<ClumpCacheKey, KList<IrisObject>> objects = new ConcurrentHashMap<>();
private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<Boolean> ore = new AtomicCache<>();
private final transient AtomicCache<KSet<String>> replaceableBlockData = new AtomicCache<>();
private final transient ConcurrentMap<ClumpCacheKey, KList<IrisObject>> 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<String> 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<String> includedBiomes = new KList<>();
@ArrayType(min = 1, type = String.class)
@Desc("Optional Iris biome keys or vanilla derivative ids denied for this deposit.")
private KList<String> 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<IrisObject> 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<IrisObject> 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<BlockPosition> 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<BlockPosition> 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<BlockPosition> 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<String> resolveReplaceableBlocks() {
KSet<String> 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<String> 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) {
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.object;
public enum IrisDepositHeightDistribution {
CLIPPED_UNIFORM,
UNIFORM,
TRIANGLE
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.object;
public enum IrisDepositPlacementScope {
TERRAIN,
ABOVE_TERRAIN,
FULL_HEIGHT
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.object;
public enum IrisDepositShape {
IRIS,
VANILLA_ELLIPSOID,
VANILLA_SCATTERED
}
@@ -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<KList<IrisObject>> 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<IrisObject> getVariantObjects(IrisData data) {
return variantCache.aquire(() -> {
KList<IrisObject> baked = new KList<>();
@@ -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
}
@@ -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;
}
}
@@ -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()) {
@@ -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);
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);
}
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);
}
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<Vector3i, FormationCanvas.Role> cells = new HashMap<>(canvas.getCells());
for (Map.Entry<Vector3i, FormationCanvas.Role> 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());
}
}
}
}
@@ -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<VolmitSender> 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() {
@@ -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) {
public void sendProgress(double percent, String thing) {
int l = 44;
int g = (int) (1D * l);
if (titleSurface == HudSurface.TITLE) {
sendTitle(C.IRIS + thing + " ", 0, 500, 250);
}
if (barSurface == HudSurface.ACTION_BAR) {
int g = (int) ((percent < 0 ? 1D : percent) * l);
sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + "<underlined> " + Form.repeat(" ", g) + "<reset>" + Form.repeat(" ", l - g));
}
} else {
int l = 44;
int g = (int) (percent * l);
if (titleSurface == HudSurface.TITLE) {
sendTitle(C.IRIS + thing + " " + C.BLUE + "<font:minecraft:uniform>" + Form.pc(percent, 0), 0, 500, 250);
}
if (barSurface == HudSurface.ACTION_BAR) {
sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + "<underlined> " + Form.repeat(" ", g) + "<reset>" + 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 <T> void showWaiting(String passive, CompletableFuture<T> 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<Void> 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()) {
@@ -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) {
sender.sendProgress(getProgress(), getName());
BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L);
} else if (barSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:job");
}
} 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) {
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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": "מחדש",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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": "再生成",
+27 -3
View File
@@ -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": "재생성",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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.",
+27 -3
View File
@@ -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.",
+27 -3
View File
@@ -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": "Реген",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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",
+27 -3
View File
@@ -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": "瑞根,你好吗?",
+27 -3
View File
@@ -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": "瑞根,你好嗎?",
@@ -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<String, String> loaded = IrisDatapackCompiler.computeRegistryRequirements(
List.of(installedPack.toFile()),
fixer);
Map<String, String> 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<String, String> loaded = IrisDatapackCompiler.computeRegistryRequirements(
List.of(pack.toFile()),
fixer);
createPack(pack, "overworld", "forest_custom", "NETHER");
Map<String, String> changedDimensionType = IrisDatapackCompiler.computeRegistryRequirements(
List.of(pack.toFile()),
fixer);
assertFalse(ServerConfigurator.loadedRegistrySatisfies(loaded, changedDimensionType));
createPack(pack, "overworld", "new_custom", "NORMAL");
Map<String, String> 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<String, String> 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();
@@ -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<String, String> 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");
@@ -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());
@@ -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(
@@ -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);"));
}
@@ -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;
}
}
@@ -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"));
}
}
@@ -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;
}
}
@@ -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<PlatformBlockState> 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<PlatformBlockState> 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);
@@ -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<PlatformBlockState> output = output(support, targetAir);
fixture.shoreline.decorate(0, 0, 0, 1, -1, 0, 1, -1,
@@ -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<Long, NativeStructureOwnershipBundle> 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;
}
}
@@ -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));
}
}
}
@@ -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<IrisBiome> 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<Matter> 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<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>();
Map<String, IrisBiome> 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<PlatformBlockState> 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<PlatformBlockState> 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;
}
}
@@ -68,10 +68,12 @@ public class IrisCarveModifierInferenceIsolationTest {
Map<String, IrisBiome> 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,
@@ -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<String, MatterCavern> 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];
@@ -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<PlatformBlockState> 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));
}
}
@@ -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());
}
}
@@ -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)));
}
}
@@ -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);
@@ -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);
@@ -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<String> 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<Vector3i> remaining = new HashSet<>(canvas.getCells().keySet());
int components = 0;
while (!remaining.isEmpty()) {
Vector3i start = remaining.iterator().next();
remaining.remove(start);
ArrayDeque<Vector3i> 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<Vector3i, FormationCanvas.Role> 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<Vector3i> neighbors(Vector3i position) {
Set<Vector3i> 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);
}
}