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)); 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 @Test
public void vacuumFitsOnlyProcessedSolidTemplateColumns() throws Exception { public void vacuumFitsOnlyProcessedSolidTemplateColumns() throws Exception {
StructureTemplate sparseTemplate = template(List.of( StructureTemplate sparseTemplate = template(List.of(
@@ -2171,6 +2208,19 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
if (methodName.equals("getSeed")) { if (methodName.equals("getSeed")) {
return TEST_SEED; 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")) { if (methodName.equals("getLevel")) {
return null; return null;
} }
@@ -93,8 +93,8 @@ import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.function.NastyRunnable; import art.arcane.volmlib.util.function.NastyRunnable;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; 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.HudBossBarLane;
import art.arcane.volmlib.util.hud.HudSlotService;
import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.io.InstanceState; import art.arcane.volmlib.util.io.InstanceState;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
@@ -549,7 +549,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
SimdSupport.install(); SimdSupport.install();
services = new KMap<>(); services = new KMap<>();
setupAudience(); setupAudience();
BukkitPlatform.hostHud(new HudSlotService(this), new HudBossBarLane()); BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane());
Bindings.setupSentry(); Bindings.setupSentry();
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered // 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, // 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(); removeShutdownHook();
} }
if (BukkitPlatform.hasHud()) { if (BukkitPlatform.hasHud()) {
BukkitPlatform.hudSlots().shutdown(); BukkitPlatform.hudBar().shutdown();
BukkitPlatform.hudLanes().shutdown(); BukkitPlatform.hudLanes().shutdown();
} }
if (configHotloadEngine != null) { if (configHotloadEngine != null) {
@@ -148,10 +148,15 @@ public class CommandIris implements DirectorExecutor {
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType); IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) { 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(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType) sender().sendMessage(IrisLanguage.text(
+ " and restart the server."); BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
));
return; return;
} }
@@ -227,10 +232,15 @@ public class CommandIris implements DirectorExecutor {
: type; : type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType); IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) { 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(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType) sender().sendMessage(IrisLanguage.text(
+ " and restart the server."); BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
));
return; return;
} }
@@ -77,18 +77,12 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O; import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; 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.Bukkit;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode; import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode; import org.bukkit.GameMode;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
@@ -108,7 +102,6 @@ import java.util.Objects;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier; import java.util.function.Supplier;
import art.arcane.iris.core.localization.IrisLanguage; 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 // previously leaked the whole sampler ForkJoinPool, the self-rescheduling
// progress task and both HUD claims for the rest of the server's uptime. // progress task and both HUD claims for the rest of the server's uptime.
MultiBurst multiBurst = null; MultiBurst multiBurst = null;
HudSlotClaim titleClaim = null; boolean progressShown = false;
HudSlotClaim barClaim = null;
int c = -1; int c = -1;
try { try {
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0))); engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
@@ -354,31 +346,11 @@ public class CommandStudio implements DirectorExecutor {
var loc = player.getLocation(); var loc = player.getLocation();
int totalTasks = d * d; int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0); AtomicInteger completedTasks = new AtomicInteger(0);
titleClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest("iris:job", HudPriority.PROGRESS, 1200L, List.of(HudSurface.TITLE))); progressShown = true;
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);
c = J.ar(() -> { 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; double jobProgress = (double) completedTasks.get() / totalTasks;
HudSurface barSurface = finalBarClaim.granted(); sender.sendProgress(jobProgress, IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS));
sender.sendProgress( BukkitPlatform.showProgressLane(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, 4000L);
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");
}
}, 0); }, 0);
new Spiraler(d, d, (x, z) -> executor.queue(() -> { new Spiraler(d, d, (x, z) -> executor.queue(() -> {
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8); var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
@@ -399,11 +371,8 @@ public class CommandStudio implements DirectorExecutor {
if (c != -1) { if (c != -1) {
J.car(c); J.car(c);
} }
if (titleClaim != null) { if (progressShown) {
titleClaim.release(); sender.sendAction(" ");
}
if (barClaim != null) {
barClaim.release();
BukkitPlatform.hudLanes().hide(player, "iris:job"); BukkitPlatform.hudLanes().hide(player, "iris:job");
} }
if (multiBurst != null) { if (multiBurst != null) {
@@ -7,6 +7,8 @@ import org.junit.Test;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import static org.junit.Assert.assertEquals; 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)); assertEquals(Long.valueOf(Long.MAX_VALUE), handler.parse(Long.toString(Long.MAX_VALUE), false));
assertThrows(DirectorParsingException.class, () -> handler.parse("9223372036854775808", 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 double SURFACE_TERRAIN_FALLOFF = 2.0;
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L; private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
private static final int SURFACE_TERRAIN_RADIUS = 12; 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 = private static final Set<String> WARNED_VACUUM_BUDGET =
ConcurrentHashMap.newKeySet(); ConcurrentHashMap.newKeySet();
@@ -45,23 +47,24 @@ public final class NativeStructureSurfaceFitter {
List<NativeStructureTerrainIntegrator.TerrainTarget> targets, List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight) { IntBinaryOperator surfaceHeight) {
return prepareSurfaceStructures( return prepareSurfaceStructures(
world, area, targets, surfaceHeight, MAX_VACUUM_TEMPLATE_CELLS); world, area, targets, surfaceHeight, MAX_SURFACE_TEMPLATE_CELLS);
} }
static VacuumFoundationPlan prepareSurfaceStructures( static VacuumFoundationPlan prepareSurfaceStructures(
WorldGenLevel world, BoundingBox area, WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets, List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
IntBinaryOperator surfaceHeight, int maximumVacuumTemplateCells) { IntBinaryOperator surfaceHeight, int maximumTemplateCells) {
if (targets == null || targets.isEmpty()) { if (targets == null || targets.isEmpty()) {
return VacuumFoundationPlan.empty(); return VacuumFoundationPlan.empty();
} }
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver"); 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()) { if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight); fitSurfaceTerrain(world, area, anchors, surfaceHeight);
} }
VacuumFootprint vacuum = collectVacuumFootprint( VacuumFootprint vacuum = collectVacuumFootprint(
world, area, targets, maximumVacuumTemplateCells); world, area, targets, maximumTemplateCells);
if (!vacuum.anchors().isEmpty()) { if (!vacuum.anchors().isEmpty()) {
fitVacuumTerrain(world, area, vacuum, surfaceHeight); fitVacuumTerrain(world, area, vacuum, surfaceHeight);
} }
@@ -223,7 +226,14 @@ public final class NativeStructureSurfaceFitter {
} }
private static List<SurfaceAnchor> collectSourceSurfaceAnchors( 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<>(); List<SurfaceAnchor> anchors = new ArrayList<>();
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) { for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
if (!requiresSourceSurfaceTerrain(target)) { if (!requiresSourceSurfaceTerrain(target)) {
@@ -231,13 +241,34 @@ public final class NativeStructureSurfaceFitter {
} }
StructureStart start = target.start(); StructureStart start = target.start();
TerrainAdjustment adjustment = start.getStructure().terrainAdaptation(); 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()) { for (StructurePiece piece : start.getPieces()) {
if (piece instanceof PoolElementStructurePiece poolPiece) { 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(); BoundingBox bounds = poolPiece.getBoundingBox();
anchors.add(surfaceAnchor( anchors.add(surfaceAnchor(
bounds, bounds.minY() + poolPiece.getGroundLevelDelta(), bounds, bounds.minY() + poolPiece.getGroundLevelDelta(),
2, adjustment)); 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()) { for (JigsawJunction junction : poolPiece.getJunctions()) {
anchors.add(new SurfaceAnchor( anchors.add(new SurfaceAnchor(
@@ -255,6 +286,42 @@ public final class NativeStructureSurfaceFitter {
return List.copyOf(anchors); 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( private static VacuumFootprint collectVacuumFootprint(
WorldGenLevel world, BoundingBox area, WorldGenLevel world, BoundingBox area,
List<NativeStructureTerrainIntegrator.TerrainTarget> targets, List<NativeStructureTerrainIntegrator.TerrainTarget> targets,
@@ -286,7 +353,7 @@ public final class NativeStructureSurfaceFitter {
Map<Long, Integer> targetCaps = new HashMap<>(); Map<Long, Integer> targetCaps = new HashMap<>();
Set<Long> targetFoundationBases = new HashSet<>(); Set<Long> targetFoundationBases = new HashSet<>();
Set<Long> targetOccupiedCells = new HashSet<>(); Set<Long> targetOccupiedCells = new HashSet<>();
VacuumCellBudget budget = new VacuumCellBudget(maximumTemplateCells); TemplateCellBudget budget = new TemplateCellBudget(maximumTemplateCells);
try { try {
for (StructurePiece piece : start.getPieces()) { for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox(); BoundingBox bounds = piece.getBoundingBox();
@@ -330,7 +397,7 @@ public final class NativeStructureSurfaceFitter {
cell.x(), cell.y(), cell.z())); cell.x(), cell.y(), cell.z()));
} }
} }
} catch (VacuumBudgetExceeded ignored) { } catch (TemplateBudgetExceeded ignored) {
warnVacuumBudget(target); warnVacuumBudget(target);
continue; continue;
} }
@@ -837,31 +904,31 @@ public final class NativeStructureSurfaceFitter {
} }
} }
private static final class VacuumCellBudget { private static final class TemplateCellBudget {
private final int maximum; private final int maximum;
private int consumed; private int consumed;
private VacuumCellBudget(int maximum) { private TemplateCellBudget(int maximum) {
if (maximum < 0) { if (maximum < 0) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Native structure vacuum cell budget cannot be negative"); "Native structure surface template budget cannot be negative");
} }
this.maximum = maximum; this.maximum = maximum;
} }
private void consume(int amount) { private void consume(int amount) {
if (amount < 0 || consumed > maximum - amount) { if (amount < 0 || consumed > maximum - amount) {
throw VacuumBudgetExceeded.INSTANCE; throw TemplateBudgetExceeded.INSTANCE;
} }
consumed += amount; consumed += amount;
} }
} }
private static final class VacuumBudgetExceeded extends RuntimeException { private static final class TemplateBudgetExceeded extends RuntimeException {
private static final VacuumBudgetExceeded INSTANCE = private static final TemplateBudgetExceeded INSTANCE =
new VacuumBudgetExceeded(); new TemplateBudgetExceeded();
private VacuumBudgetExceeded() { private TemplateBudgetExceeded() {
super(null, null, false, false); super(null, null, false, false);
} }
} }
@@ -89,6 +89,24 @@ final class NativeStructureTemplateOccupancy {
return lowest; 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) { static boolean isSolidBase(BlockState state) {
return state != null && state.isSolid() return state != null && state.isSolid()
&& !state.is(Blocks.STRUCTURE_VOID) && !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", "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", "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", "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", "nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes",
"carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass", "carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass",
"acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button", "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) { 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) if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM)
&& (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS && (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS
|| mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) { || mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) {
@@ -7,8 +7,10 @@ import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; 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 * 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); assertNotNull(state);
assertEquals(Blocks.OAK_LOG, state.handle().getBlock()); 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/JigsawStudioToolCodec.java
art/arcane/iris/core/service/ObjectSVC.java art/arcane/iris/core/service/ObjectSVC.java
art/arcane/iris/core/service/ObjectStudioSaveService.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/StudioSVC.java
art/arcane/iris/core/service/TreeSVC.java art/arcane/iris/core/service/TreeSVC.java
art/arcane/iris/core/structure/BulkStructureImporter.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/IrisReflectiveAPI.java
art/arcane/iris/core/tools/IrisToolbelt.java art/arcane/iris/core/tools/IrisToolbelt.java
art/arcane/iris/core/tools/IrisWorldCreator.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/IrisEngineEffects.java
art/arcane/iris/engine/IrisWorldManager.java art/arcane/iris/engine/IrisWorldManager.java
art/arcane/iris/engine/MarkerSpawnScanner.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.nms.datapack.IDataFixer;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher; import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDirectoryResolver; 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.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
@@ -31,17 +33,22 @@ import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
public final class IrisDatapackCompiler { public final class IrisDatapackCompiler {
private static final int INPUT_FINGERPRINT_SCHEMA = 2; private static final int INPUT_FINGERPRINT_SCHEMA = 2;
private static final int INPUT_BUFFER_BYTES = 64 * 1024; private static final int INPUT_BUFFER_BYTES = 64 * 1024;
private static final int WORLD_PACK_SCAN_DEPTH = 8; 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 List<String> INPUT_DIRECTORIES = List.of("dimensions", "biomes", "snippet");
private static final String FLAT_VOID_LEVEL_STEM = """ private static final String FLAT_VOID_LEVEL_STEM = """
{ {
@@ -164,6 +171,117 @@ public final class IrisDatapackCompiler {
return HexFormat.of().formatHex(digest.digest()); 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) { public static String compilerIdentity(IDataFixer fixer) {
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer"); IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
return String.join( return String.join(
@@ -91,6 +91,7 @@ public class ServerConfigurator {
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024; private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
private static volatile boolean loadedDatapackRuntimeReady; private static volatile boolean loadedDatapackRuntimeReady;
private static volatile String loadedDatapackCompilerInputFingerprint = ""; private static volatile String loadedDatapackCompilerInputFingerprint = "";
private static volatile Map<String, String> loadedDatapackRegistryRequirements = Map.of();
private static volatile long loadedDatapackRuntimeGeneration; private static volatile long loadedDatapackRuntimeGeneration;
private static volatile boolean loadedDatapackRestartRequired; private static volatile boolean loadedDatapackRestartRequired;
@@ -98,6 +99,7 @@ public class ServerConfigurator {
synchronized (DATAPACK_INSTALL_LOCK) { synchronized (DATAPACK_INSTALL_LOCK) {
invalidateLoadedDatapackRuntime(); invalidateLoadedDatapackRuntime();
loadedDatapackCompilerInputFingerprint = ""; loadedDatapackCompilerInputFingerprint = "";
loadedDatapackRegistryRequirements = Map.of();
loadedDatapackRestartRequired = false; loadedDatapackRestartRequired = false;
} }
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration(); IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
@@ -112,13 +114,15 @@ public class ServerConfigurator {
if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) { if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) {
loadedDatapackRuntimeReady = !IrisSettings.get().getGeneral().adjustVanillaHeight loadedDatapackRuntimeReady = !IrisSettings.get().getGeneral().adjustVanillaHeight
&& pinLoadedDatapackCompilerInputs( && pinLoadedDatapackCompilerInputs(
DefaultPackBootstrapProvisioner.compilerInputFingerprintThisStartup()); DefaultPackBootstrapProvisioner.compilerInputFingerprintThisStartup())
&& pinLoadedDatapackRegistryRequirements();
IrisLogging.info("Paper loaded the Iris datapack during bootstrap; skipping the legacy startup install."); IrisLogging.info("Paper loaded the Iris datapack during bootstrap; skipping the legacy startup install.");
} else { } else {
DatapackInstallResult result = installDataPacks(true); DatapackInstallResult result = installDataPacks(true);
loadedDatapackRuntimeReady = result.succeeded() loadedDatapackRuntimeReady = result.succeeded()
&& !result.restartRequired() && !result.restartRequired()
&& pinLoadedDatapackCompilerInputs(); && pinLoadedDatapackCompilerInputs()
&& pinLoadedDatapackRegistryRequirements();
if (result.restartRequired()) { if (result.restartRequired()) {
IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them."); 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())) { || INMS.get().missingDimensionTypes(requiredDimension.getDimensionTypeKey())) {
return false; return false;
} }
String currentFingerprint = computeCurrentDatapackCompilerInputFingerprint(resolveDataFixer()); Map<String, String> requiredRegistryEntries =
return reusableRuntimeFingerprint( IrisDatapackCompiler.computeRegistryRequirements(requiredDimension, resolveDataFixer());
loadedDatapackCompilerInputFingerprint, return loadedRegistrySatisfies(
currentFingerprint); loadedDatapackRegistryRequirements,
requiredRegistryEntries);
} catch (IOException | RuntimeException exception) { } 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; 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) { static boolean reusableRuntimeFingerprint(String loadedFingerprint, String currentFingerprint) {
return loadedFingerprint != null return loadedFingerprint != null
&& !loadedFingerprint.isBlank() && !loadedFingerprint.isBlank()
@@ -175,6 +175,14 @@ public final class BukkitCommandMessagesExtended {
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend", "iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend",
C.YELLOW + "Try one of: 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( 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", "iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load",
C.GREEN + "World staging completed. Iris is restarting the server to generate/load \"" + "{worldName}" + "\"." 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_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2,
COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS, COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS,
COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND, 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_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD,
COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS, COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS,
COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD, 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( public static final TextKey STUDIO_S_V_C_INSTALLING_PACKAGE = TextKey.of(
"iris.bukkit.runtime.studiosvc.installing_package", "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( public static final TextKey STUDIO_S_V_C_LOOKING_PACKAGE = TextKey.of(
"iris.bukkit.runtime.studiosvc.looking_package", "iris.bukkit.runtime.studiosvc.looking_package",
@@ -701,6 +709,8 @@ public final class BukkitRuntimeMessages {
IRIS_CONVERTER_CONVERTED_3, IRIS_CONVERTER_CONVERTED_3,
IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS, IRIS_CONVERTER_SOME_SCHEMATICS_FAILED_CONVERT_CHECK_CONSOLE_DETAILS,
STUDIO_S_V_C_INSTALLING_PACKAGE, 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_LOOKING_PACKAGE,
STUDIO_S_V_C_FOUND_IRIS_FOLDER, STUDIO_S_V_C_FOUND_IRIS_FOLDER,
STUDIO_S_V_C_FOUND_DIMENSION_FOLDER_REPACKAGING, 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_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 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_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_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_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_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_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 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"); 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_FINALIZE_OPEN,
STUDIO_STAGE_CLEANUP, STUDIO_STAGE_CLEANUP,
WORLD_CREATE_TELEPORT_FAILED, WORLD_CREATE_TELEPORT_FAILED,
WORLD_CREATE_ACTION, WORLD_CREATE_BOSSBAR_WORKING,
WORLD_CREATE_CONSOLE, 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_ACTION,
WORLD_PREGEN_CONSOLE, WORLD_PREGEN_CONSOLE,
CHUNK_TITLE_REGEN, 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. * engine skips them, and this validator surfaces the mistake to the author at validate time.
*/ */
final class PackBiomeLayerValidator { 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 static final int DEFAULT_LAYER_COUNT = 1;
private PackBiomeLayerValidator() { private PackBiomeLayerValidator() {
@@ -58,8 +57,8 @@ final class PackBiomeLayerValidator {
continue; continue;
} }
Integer layers = arrayLength(biome, "layers", biomeKey, blockingErrors); Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors);
Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, blockingErrors); Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors);
if (layers == null || ceiling == null) { if (layers == null || ceiling == null) {
continue; continue;
} }
@@ -72,9 +71,65 @@ final class PackBiomeLayerValidator {
return blockingErrors; 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)) { if (!biome.has(field) || biome.isNull(field)) {
return DEFAULT_LAYER_COUNT; return defaultCount;
} }
JSONArray array = biome.optJSONArray(field); JSONArray array = biome.optJSONArray(field);
@@ -99,6 +99,8 @@ public final class PackValidator {
blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns( blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns(
new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory)); new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory));
blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes"))); 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); PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder);
addDistinct(blockingErrors, styledRanges.errors()); addDistinct(blockingErrors, styledRanges.errors());
addDistinct(warnings, styledRanges.warnings()); 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.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeProgressMessages; 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.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form; 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.localization.MessageArgument;
import org.bukkit.Bukkit; 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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@@ -53,7 +45,6 @@ final class StudioOpenProgressReporter {
AtomicLong startMs = new AtomicLong(System.currentTimeMillis()); AtomicLong startMs = new AtomicLong(System.currentTimeMillis());
AtomicInteger taskId = new AtomicInteger(-1); AtomicInteger taskId = new AtomicInteger(-1);
org.bukkit.boss.BossBar bossBar; org.bukkit.boss.BossBar bossBar;
HudSlotClaim loaderClaim;
if (sender.isPlayer() && sender.player() != null) { if (sender.isPlayer() && sender.player() != null) {
bossBar = Bukkit.createBossBar( bossBar = Bukkit.createBossBar(
@@ -64,15 +55,8 @@ final class StudioOpenProgressReporter {
bossBar.setProgress(0.0D); bossBar.setProgress(0.0D);
bossBar.addPlayer(sender.player()); bossBar.addPlayer(sender.player());
bossBar.setVisible(true); 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 { } else {
bossBar = null; bossBar = null;
loaderClaim = null;
} }
int scheduledTaskId = J.ar(() -> { int scheduledTaskId = J.ar(() -> {
@@ -95,26 +79,14 @@ final class StudioOpenProgressReporter {
J.a(() -> { J.a(() -> {
bossBar.removeAll(); bossBar.removeAll();
bossBar.setVisible(false); bossBar.setVisible(false);
loaderClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
}, 60); }, 60);
} }
if (sender.isPlayer()) { if (sender.isPlayer()) {
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text( sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_FAILED, RuntimeProgressMessages.STUDIO_ACTION_FAILED,
MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)),
MessageArgument.trusted("stage", currentStage) 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 { } else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2)); sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED_2));
} }
@@ -126,26 +98,14 @@ final class StudioOpenProgressReporter {
J.a(() -> { J.a(() -> {
bossBar.removeAll(); bossBar.removeAll();
bossBar.setVisible(false); bossBar.setVisible(false);
loaderClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
}, 60); }, 60);
} }
if (sender.isPlayer()) { if (sender.isPlayer()) {
HudSurface loaderSurface = loaderClaim.resolve();
if (loaderSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:studio-open");
sender.sendAction(IrisLanguage.text( sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_READY, RuntimeProgressMessages.STUDIO_ACTION_READY,
MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)), MessageArgument.trusted("bar", buildStudioProgressBar(1.0D)),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 1)) 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 { } else {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_READY, MessageArgument.untrusted("value", String.valueOf(Form.duration(elapsed, 1))))); 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( sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.STUDIO_ACTION_PROGRESS, RuntimeProgressMessages.STUDIO_ACTION_PROGRESS,
MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)), MessageArgument.trusted("bar", buildStudioProgressBar(currentProgress)),
@@ -172,15 +129,6 @@ final class StudioOpenProgressReporter {
MessageArgument.trusted("stage", currentStage), MessageArgument.trusted("stage", currentStage),
MessageArgument.trusted("elapsed", Form.duration(elapsed, 0)) 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 { } else {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get(); 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.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeProgressMessages; import art.arcane.iris.core.localization.RuntimeProgressMessages;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.ChunkSpiral; import art.arcane.iris.util.common.math.ChunkSpiral;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form; 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.localization.MessageArgument;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.World; import org.bukkit.World;
@@ -62,7 +57,6 @@ public final class ChunkJobReporter {
private final AtomicInteger failures = new AtomicInteger(0); private final AtomicInteger failures = new AtomicInteger(0);
private volatile int total = 0; private volatile int total = 0;
private volatile long startMs = 0L; private volatile long startMs = 0L;
private volatile HudSlotClaim claim;
public ChunkJobReporter(VolmitSender sender, String title, World world) { public ChunkJobReporter(VolmitSender sender, String title, World world) {
this.sender = sender; this.sender = sender;
@@ -127,15 +121,6 @@ public final class ChunkJobReporter {
bossBar.addPlayer(sender.player()); bossBar.addPlayer(sender.player());
bossBar.setVisible(true); 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); AtomicInteger taskId = new AtomicInteger(-1);
taskId.set(J.ar(() -> { taskId.set(J.ar(() -> {
double currentProgress = Math.max(0.0D, Math.min(1.0D, progress.get())); double currentProgress = Math.max(0.0D, Math.min(1.0D, progress.get()));
@@ -157,10 +142,7 @@ public final class ChunkJobReporter {
MessageArgument.trusted("percent", percent) MessageArgument.trusted("percent", percent)
)); ));
} }
if (sender.isPlayer() && claim != null) { if (sender.isPlayer()) {
HudSurface surface = claim.resolve();
if (surface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:chunk-job");
sender.sendAction(IrisLanguage.text( sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.CHUNK_ACTION_PROGRESS, RuntimeProgressMessages.CHUNK_ACTION_PROGRESS,
MessageArgument.trusted("bar", progressBar(currentProgress)), MessageArgument.trusted("bar", progressBar(currentProgress)),
@@ -169,16 +151,6 @@ public final class ChunkJobReporter {
MessageArgument.trusted("applied", applied.get()), MessageArgument.trusted("applied", applied.get()),
MessageArgument.trusted("total", total <= 0 ? "?" : total) 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)); }, REPORT_INTERVAL_TICKS));
if (complete.get()) { if (complete.get()) {
@@ -214,30 +186,15 @@ public final class ChunkJobReporter {
J.a(() -> { J.a(() -> {
bossBar.removeAll(); bossBar.removeAll();
bossBar.setVisible(false); bossBar.setVisible(false);
HudSlotClaim finishedClaim = claim;
if (finishedClaim != null) {
finishedClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:chunk-job");
}
}, FINISH_LINGER_TICKS); }, FINISH_LINGER_TICKS);
} }
if (sender.isPlayer() && claim != null) { if (sender.isPlayer()) {
HudSurface surface = claim.resolve();
if (surface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:chunk-job");
sender.sendAction(IrisLanguage.text( sender.sendAction(IrisLanguage.text(
ok ? RuntimeProgressMessages.CHUNK_ACTION_DONE : RuntimeProgressMessages.CHUNK_ACTION_FAILED, ok ? RuntimeProgressMessages.CHUNK_ACTION_DONE : RuntimeProgressMessages.CHUNK_ACTION_FAILED,
MessageArgument.trusted("bar", progressBar(1.0D)), MessageArgument.trusted("bar", progressBar(1.0D)),
MessageArgument.trusted("summary", summary) 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( sender.sendMessage(IrisLanguage.text(
ok ? RuntimeProgressMessages.CHUNK_COMPLETE : RuntimeProgressMessages.CHUNK_FAILED, 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); IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) { if (!request.retainOnFailure()) {
updateStage(request, "cleanup", 1.00D); 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( deferFailedOpenCleanup(
entryLoadFuture, entryLoadFuture,
provider, 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) { private long logStudioPhase(StudioOpenRequest request, String phase, long t, long openStart) {
long now = System.nanoTime(); long now = System.nanoTime();
IrisLogging.info("[Studio timing] world=%s kind=%s phase=%s duration=%dms cumulative=%dms", 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.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form; 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.localization.MessageArgument;
import org.bukkit.boss.BarColor; import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle; import org.bukkit.boss.BarStyle;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; 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 INDETERMINATE_SEGMENT_WIDTH = 5;
private static final int HUD_PULSE_TICKS = 5; private static final int HUD_PULSE_TICKS = 5;
private static final int HUD_TERMINAL_TICKS = 60; 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 ACTION_INTERVAL_MILLIS = 250L;
private static final long CHAT_INTERVAL_MILLIS = 5_000L; private static final long CHAT_INTERVAL_MILLIS = 5_000L;
private static final int CHAT_PERCENT_STEP = 10; private static final int CHAT_PERCENT_STEP = 10;
@@ -47,7 +41,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
private final String hudLaneId; private final String hudLaneId;
private PackDownloader.DownloadPhase phase; private PackDownloader.DownloadPhase phase;
private PackDownloader.DownloadProgress latestProgress; private PackDownloader.DownloadProgress latestProgress;
private HudSlotClaim hudClaim; private boolean hudActive;
private long transferredBytes; private long transferredBytes;
private long transferElapsedMillis; private long transferElapsedMillis;
private long phaseStartedMillis; private long phaseStartedMillis;
@@ -93,12 +87,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
if (player == null || !BukkitPlatform.hasHud()) { if (player == null || !BukkitPlatform.hasHud()) {
return; return;
} }
hudClaim = BukkitPlatform.hudSlots().open(player, new HudSlotRequest( hudActive = true;
hudLaneId,
HudPriority.PROGRESS,
HUD_CLAIM_TTL_MILLIS,
List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)
));
int scheduledTaskId = J.ar(this::pulseHud, HUD_PULSE_TICKS); int scheduledTaskId = J.ar(this::pulseHud, HUD_PULSE_TICKS);
pulseTaskId = scheduledTaskId; pulseTaskId = scheduledTaskId;
if (finished || hudDisabled) { if (finished || hudDisabled) {
@@ -237,9 +226,8 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
private void pulseHud() { private void pulseHud() {
HudSnapshot snapshot; HudSnapshot snapshot;
HudSlotClaim claim;
synchronized (this) { synchronized (this) {
if (finished || hudDisabled || hudClaim == null) { if (finished || hudDisabled || !hudActive) {
stopPulse(); stopPulse();
return; return;
} }
@@ -249,21 +237,15 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
} }
lastActionMillis = now; lastActionMillis = now;
snapshot = hudSnapshot(now); snapshot = hudSnapshot(now);
claim = hudClaim;
} }
boolean scheduled = J.runEntity(player, () -> renderHudPulse(claim, snapshot)); boolean scheduled = J.runEntity(player, () -> renderHudPulse(snapshot));
if (!scheduled) { if (!scheduled) {
disableHud(null); disableHud(null);
} }
} }
private void renderHudPulse(HudSlotClaim claim, HudSnapshot snapshot) { private void renderHudPulse(HudSnapshot snapshot) {
try { 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( BukkitPlatform.hudLanes().show(
player, player,
hudLaneId, hudLaneId,
@@ -273,9 +255,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
BarStyle.SEGMENTED_20, BarStyle.SEGMENTED_20,
1_500L 1_500L
); );
} else { sender.sendAction(snapshot.line());
BukkitPlatform.hudLanes().hide(player, hudLaneId);
}
} catch (RuntimeException failure) { } catch (RuntimeException failure) {
disableHud(failure); disableHud(failure);
} }
@@ -315,9 +295,9 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
} }
private synchronized void deliverTerminalHud(String message, BarColor color, double progress) { private synchronized void deliverTerminalHud(String message, BarColor color, double progress) {
HudSlotClaim claim = hudClaim; boolean active = hudActive;
hudClaim = null; hudActive = false;
if (player == null || claim == null || hudDisabled) { if (player == null || !active || hudDisabled) {
return; return;
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -326,15 +306,10 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
int delayTicks = (int) Math.ceil(delayMillis / 50.0D); int delayTicks = (int) Math.ceil(delayMillis / 50.0D);
lastActionMillis = now + delayMillis; lastActionMillis = now + delayMillis;
AtomicBoolean cleaned = new AtomicBoolean(); AtomicBoolean cleaned = new AtomicBoolean();
Runnable cleanup = () -> releaseHudClaim(cleaned, claim); Runnable cleanup = () -> releaseHudLane(cleaned);
Runnable retiredCleanup = () -> retireHudClaim(cleaned, claim); Runnable retiredCleanup = () -> retireHudLane(cleaned);
Runnable display = () -> { Runnable display = () -> {
try { 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( BukkitPlatform.hudLanes().show(
player, player,
hudLaneId, hudLaneId,
@@ -344,7 +319,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
BarStyle.SOLID, BarStyle.SOLID,
4_000L 4_000L
); );
} sender.sendAction(message);
} finally { } finally {
if (!J.runEntity(player, cleanup, HUD_TERMINAL_TICKS, retiredCleanup)) { if (!J.runEntity(player, cleanup, HUD_TERMINAL_TICKS, retiredCleanup)) {
retiredCleanup.run(); retiredCleanup.run();
@@ -359,43 +334,41 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
} }
private void disableHud(Throwable failure) { private void disableHud(Throwable failure) {
HudSlotClaim claim; boolean active;
synchronized (this) { synchronized (this) {
if (hudDisabled) { if (hudDisabled) {
return; return;
} }
hudDisabled = true; hudDisabled = true;
stopPulse(); stopPulse();
claim = hudClaim; active = hudActive;
hudClaim = null; hudActive = false;
} }
if (failure != null) { if (failure != null) {
IrisLogging.reportError("Pack download HUD disabled after a delivery failure.", failure); IrisLogging.reportError("Pack download HUD disabled after a delivery failure.", failure);
} }
if (player != null && claim != null) { if (player != null && active) {
AtomicBoolean cleaned = new AtomicBoolean(); AtomicBoolean cleaned = new AtomicBoolean();
Runnable cleanup = () -> releaseHudClaim(cleaned, claim); Runnable cleanup = () -> releaseHudLane(cleaned);
Runnable retiredCleanup = () -> retireHudClaim(cleaned, claim); Runnable retiredCleanup = () -> retireHudLane(cleaned);
if (!J.runEntity(player, cleanup, 0, retiredCleanup)) { if (!J.runEntity(player, cleanup, 0, retiredCleanup)) {
retiredCleanup.run(); retiredCleanup.run();
} }
} }
} }
private void releaseHudClaim(AtomicBoolean cleaned, HudSlotClaim claim) { private void releaseHudLane(AtomicBoolean cleaned) {
if (!cleaned.compareAndSet(false, true)) { if (!cleaned.compareAndSet(false, true)) {
return; return;
} }
BukkitPlatform.hudLanes().hide(player, hudLaneId); BukkitPlatform.hudLanes().hide(player, hudLaneId);
claim.release();
} }
private void retireHudClaim(AtomicBoolean cleaned, HudSlotClaim claim) { private void retireHudLane(AtomicBoolean cleaned) {
if (!cleaned.compareAndSet(false, true)) { if (!cleaned.compareAndSet(false, true)) {
return; return;
} }
BukkitPlatform.hudLanes().retire(playerId, hudLaneId); BukkitPlatform.hudLanes().retire(playerId, hudLaneId);
claim.retire();
} }
private synchronized void stopPulse() { private synchronized void stopPulse() {
@@ -184,7 +184,9 @@ public class StudioSVC implements IrisService {
boolean replaceExisting boolean replaceExisting
) { ) {
if (J.isPrimaryThread()) { 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; return null;
} }
String dimensionKey = dimension.getLoadKey(); String dimensionKey = dimension.getLoadKey();
@@ -193,7 +195,11 @@ public class StudioSVC implements IrisService {
source = resolveSafePackSource(dimension.getLoader().getDataFolder()); source = resolveSafePackSource(dimension.getLoader().getDataFolder());
} catch (IOException e) { } catch (IOException e) {
IrisLogging.reportError("Failed to inspect source dimension pack '" + dimensionKey + "'.", 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; return null;
} }
Path target = folder.toPath().toAbsolutePath().normalize(); 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); 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; return null;
} finally { } finally {
if (validationMutation != null) { if (validationMutation != null) {
@@ -7,10 +7,6 @@ import art.arcane.iris.engine.object.IrisObject;
import art.arcane.volmlib.util.data.Varint; import art.arcane.volmlib.util.data.Varint;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form; 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.NBTUtil;
import art.arcane.volmlib.util.nbt.io.NamedTag; import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; 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.apache.commons.io.FileUtils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -38,7 +32,6 @@ import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger; 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.BukkitRuntimeMessages;
import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisLanguage;
@@ -84,41 +77,18 @@ public class IrisConverter {
int mv = objW * objH * objD; int mv = objW * objH * objD;
AtomicInteger v = new AtomicInteger(0); AtomicInteger v = new AtomicInteger(0);
boolean reportProgress = mv > 2_000_000 && sender.isPlayer(); 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 // 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 { try {
if (mv > 2_000_000) { if (mv > 2_000_000) {
largeObject = true; largeObject = true;
IrisLogging.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); IrisLogging.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob"));
IrisLogging.info(C.GRAY + "- It may take a while"); IrisLogging.info(C.GRAY + "- It may take a while");
if (reportProgress) { if (reportProgress) {
AtomicLong lastResolveMs = new AtomicLong(0L);
i = J.ar(() -> { 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; double conversionProgress = (double) v.get() / mv;
HudSurface barSurface = barClaim.granted(); sender.sendProgress(conversionProgress, IrisLanguage.text(RuntimeUiMessages.CONVERTING));
sender.sendProgress( BukkitPlatform.showProgressLane(sender.player(), "iris:job", IrisLanguage.text(RuntimeUiMessages.CONVERTING) + " " + Form.pc(conversionProgress, 0), conversionProgress, 4000L);
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");
}
}, 0); }, 0);
} }
} }
@@ -154,11 +124,8 @@ public class IrisConverter {
} finally { } finally {
if (i != -1) J.car(i); if (i != -1) J.car(i);
if (titleClaim != null) { if (reportProgress) {
titleClaim.release(); sender.sendAction(" ");
}
if (barClaim != null) {
barClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); 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.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form; 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.localization.MessageArgument;
import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.iris.util.common.plugin.VolmitSender; 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.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.IntSupplier; import java.util.function.IntSupplier;
@@ -163,6 +158,9 @@ public class IrisCreator {
if (Bukkit.isPrimaryThread()) { if (Bukkit.isPrimaryThread()) {
throw new IrisException("You cannot invoke create() on the main thread."); throw new IrisException("You cannot invoke create() on the main thread.");
} }
if (sender == null) {
sender = BukkitPlatform.console();
}
NamespacedKey worldKey; NamespacedKey worldKey;
try { try {
worldKey = IrisWorldStorage.managedKeyFromName(name); worldKey = IrisWorldStorage.managedKeyFromName(name);
@@ -170,15 +168,20 @@ public class IrisCreator {
throw new IrisException(e.getMessage(), e); throw new IrisException(e.getMessage(), e);
} }
name = IrisWorldStorage.logicalName(worldKey); name = IrisWorldStorage.logicalName(worldKey);
WorldCreationProgressReporter creationReporter = !studio && !benchmark
? WorldCreationProgressReporter.start(sender, name)
: null;
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get(); LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
LifecycleOperationCoordinator.Lease worldLease = null; LifecycleOperationCoordinator.Lease worldLease = null;
try { try {
reportStudioProgress(0.02D, "resolve_dimension"); reportStudioProgress(0.02D, "resolve_dimension");
reportCreationProgress(creationReporter, 0.02D, "resolve_dimension");
IrisDimension resolvedDimension = IrisToolbelt.getDimension(dimension()); IrisDimension resolvedDimension = IrisToolbelt.getDimension(dimension());
if (resolvedDimension == null) { if (resolvedDimension == null) {
throw new IrisException("Dimension cannot be found for id " + dimension()); throw new IrisException("Dimension cannot be found for id " + dimension());
} }
reportCreationProgress(creationReporter, 0.06D, "validate_pack");
IrisStartupValidation.requireWorldCreationReady(); IrisStartupValidation.requireWorldCreationReady();
PackValidationRegistry.requireLoadable( PackValidationRegistry.requireLoadable(
resolvedDimension.getLoader().getDataFolder().getName()); resolvedDimension.getLoader().getDataFolder().getName());
@@ -186,9 +189,31 @@ public class IrisCreator {
LifecycleOperationCoordinator.Domain.WORLD_MUTATION, LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
LifecycleOperationCoordinator.OperationKind.WORLD_CREATE, LifecycleOperationCoordinator.OperationKind.WORLD_CREATE,
worldKey.toString()); 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) { } catch (LifecycleOperationCoordinator.BusyException e) {
if (creationReporter != null) {
creationReporter.fail();
}
throw new IrisException(e.getMessage(), e); 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 { } finally {
if (worldLease != null) { if (worldLease != null) {
worldLease.close(); 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 dimensionRoot;
File storageRoot; File storageRoot;
try { try {
@@ -213,16 +242,13 @@ public class IrisCreator {
if (Files.exists(storageRoot.toPath()) || WorldIdentity.resolve(worldKey).isPresent()) { if (Files.exists(storageRoot.toPath()) || WorldIdentity.resolve(worldKey).isPresent()) {
throw new IrisException("World \"" + name + "\" already exists or is loaded."); throw new IrisException("World \"" + name + "\" already exists or is loaded.");
} }
if (sender == null) {
sender = BukkitPlatform.console();
}
World world = null; World world = null;
boolean bukkitRegistered = false; boolean bukkitRegistered = false;
PlatformChunkGenerator stagedGenerator = null; PlatformChunkGenerator stagedGenerator = null;
try { try {
reportStudioProgress(0.08D, "resolve_dimension"); reportStudioProgress(0.08D, "resolve_dimension");
reportStudioProgress(0.16D, "prepare_world_pack"); reportStudioProgress(0.16D, "prepare_world_pack");
reportCreationProgress(creationReporter, 0.10D, "install_datapacks");
DatapackInstallResult datapackResult = prepareDatapacks(resolvedDimension); DatapackInstallResult datapackResult = prepareDatapacks(resolvedDimension);
if (!datapackResult.succeeded()) { if (!datapackResult.succeeded()) {
throw new IrisException("Failed to compile datapacks for dimension \"" + dimension() + "\"."); throw new IrisException("Failed to compile datapacks for dimension \"" + dimension() + "\".");
@@ -235,6 +261,7 @@ public class IrisCreator {
IrisDimension installedDimension = resolvedDimension; IrisDimension installedDimension = resolvedDimension;
if (!studio() || benchmark) { if (!studio() || benchmark) {
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
installedDimension = IrisServices.get(StudioSVC.class) installedDimension = IrisServices.get(StudioSVC.class)
.installIntoWorld(sender, resolvedDimension, dimensionRoot); .installIntoWorld(sender, resolvedDimension, dimensionRoot);
if (installedDimension == null) { if (installedDimension == null) {
@@ -249,6 +276,7 @@ public class IrisCreator {
} }
reportStudioProgress(0.28D, "install_datapacks"); reportStudioProgress(0.28D, "install_datapacks");
reportCreationProgress(creationReporter, 0.36D, "prepare_generator");
AtomicDouble pp = new AtomicDouble(0); AtomicDouble pp = new AtomicDouble(0);
AtomicBoolean done = new AtomicBoolean(false); AtomicBoolean done = new AtomicBoolean(false);
long generatorPrepareStart = System.nanoTime(); long generatorPrepareStart = System.nanoTime();
@@ -267,12 +295,10 @@ public class IrisCreator {
throw new IrisException("Access is null. Something bad happened."); throw new IrisException("Access is null. Something bad happened.");
} }
stagedGenerator = access; stagedGenerator = access;
HudSlotClaim createClaim = !benchmark && studioProgressConsumer == null && sender.isPlayer() AtomicInteger createProgressTask = startCreateProgressReporter(access, done, creationReporter);
? openLoaderClaim("iris:world-create")
: null;
AtomicInteger createProgressTask = startCreateProgressReporter(access, done, createClaim);
reportStudioProgress(0.46D, "create_world"); reportStudioProgress(0.46D, "create_world");
reportCreationProgress(creationReporter, 0.44D, "create_world");
long nmsStartNanos = System.nanoTime(); long nmsStartNanos = System.nanoTime();
try { try {
WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE; WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE;
@@ -283,7 +309,6 @@ public class IrisCreator {
} catch (Throwable e) { } catch (Throwable e) {
done.set(true); done.set(true);
cancelRepeatingTask(createProgressTask); cancelRepeatingTask(createProgressTask);
releaseLoaderClaim(createClaim, "iris:world-create");
if (e instanceof TimeoutException) { if (e instanceof TimeoutException) {
ServerConfigurator.restart("World creation timed out for \"" + name + "\"."); ServerConfigurator.restart("World creation timed out for \"" + name + "\".");
} }
@@ -302,8 +327,8 @@ public class IrisCreator {
done.set(true); done.set(true);
cancelRepeatingTask(createProgressTask); cancelRepeatingTask(createProgressTask);
releaseLoaderClaim(createClaim, "iris:world-create");
reportStudioProgress(0.86D, "create_world"); reportStudioProgress(0.86D, "create_world");
reportCreationProgress(creationReporter, 0.84D, "register_world");
if (!studio && !benchmark) { if (!studio && !benchmark) {
BukkitWorldConfiguration.register( BukkitWorldConfiguration.register(
@@ -327,6 +352,7 @@ public class IrisCreator {
throw e; throw e;
} }
} }
reportCreationProgress(creationReporter, 0.92D, "teleport_player");
awaitSenderTeleport(world); awaitSenderTeleport(world);
if (pregen != null) { if (pregen != null) {
@@ -336,20 +362,26 @@ public class IrisCreator {
.whenDone(() -> ff.complete(true)); .whenDone(() -> ff.complete(true));
AtomicBoolean dx = new AtomicBoolean(false); AtomicBoolean dx = new AtomicBoolean(false);
HudSlotClaim pregenClaim = sender.isPlayer() ? openLoaderClaim("iris:pregen") : null; boolean pregenHud = creationReporter == null && sender.isPlayer();
AtomicInteger pregenProgressTask = startPregenProgressReporter(pp, dx, pregenClaim); AtomicInteger pregenProgressTask = startPregenProgressReporter(
pp,
dx,
pregenHud,
creationReporter
);
try { try {
ff.get(); ff.get();
dx.set(true); dx.set(true);
cancelRepeatingTask(pregenProgressTask); cancelRepeatingTask(pregenProgressTask);
releaseLoaderClaim(pregenClaim, "iris:pregen"); hidePregenLane(pregenHud);
} catch (Throwable e) { } catch (Throwable e) {
dx.set(true); dx.set(true);
cancelRepeatingTask(pregenProgressTask); cancelRepeatingTask(pregenProgressTask);
releaseLoaderClaim(pregenClaim, "iris:pregen"); hidePregenLane(pregenHud);
IrisLogging.reportError(e); IrisLogging.reportError(e);
} }
} }
reportCreationProgress(creationReporter, 0.99D, "finalize");
return world; return world;
} catch (Throwable failure) { } catch (Throwable failure) {
rollbackWorldCreation(worldKey, world, stagedGenerator, storageRoot, bukkitRegistered, 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) { private void reportStudioTiming(String phase, long startedAtNanos) {
BiConsumer<String, Long> consumer = studioTimingConsumer; BiConsumer<String, Long> consumer = studioTimingConsumer;
if (consumer == null) { if (consumer == null) {
@@ -498,7 +540,11 @@ public class IrisCreator {
return ServerConfigurator.installDataPacksIfChanged(true, studioTimingConsumer); 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); AtomicInteger taskId = new AtomicInteger(-1);
if (benchmark) { if (benchmark) {
return taskId; return taskId;
@@ -510,10 +556,9 @@ public class IrisCreator {
} }
return access.getEngine().getGenerated(); return access.getEngine().getGenerated();
}; };
AtomicLong lastResolveMs = new AtomicLong(0L);
access.getSpawnChunks().whenComplete((required, throwable) -> { access.getSpawnChunks().whenComplete((required, throwable) -> {
if (throwable != null) { 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; return;
} }
@@ -521,7 +566,7 @@ public class IrisCreator {
return; return;
} }
int interval = studioProgressConsumer != null || sender.isPlayer() ? 1 : 20; int interval = studioProgressConsumer != null || creationReporter != null && sender.isPlayer() ? 1 : 20;
taskId.set(J.ar(() -> { taskId.set(J.ar(() -> {
if (done.get()) { if (done.get()) {
cancelRepeatingTask(taskId); cancelRepeatingTask(taskId);
@@ -537,57 +582,27 @@ public class IrisCreator {
double progress = (double) generated / required; double progress = (double) generated / required;
if (studioProgressConsumer != null) { if (studioProgressConsumer != null) {
reportStudioProgress(0.40D + (0.42D * progress), "create_world"); reportStudioProgress(0.40D + (0.42D * progress), "create_world");
return;
} }
int percent = (int) Math.round(progress * 100.0D); if (creationReporter != null) {
int remaining = required - generated; creationReporter.update(
if (sender.isPlayer() && claim != null) { 0.44D + (0.38D * progress),
HudSurface surface = resolveThrottled(claim, lastResolveMs); "create_world",
if (surface == HudSurface.ACTION_BAR) { C.DARK_GRAY + " (" + Form.f(generated) + "/" + Form.f(required) + " chunks)"
BukkitPlatform.hudLanes().hide(sender.player(), "iris:world-create"); );
int barWidth = 44;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * barWidth);
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int bi = 0; bi < barWidth; bi++) {
bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|");
} }
bar.append(C.DARK_GRAY).append("]");
sender.sendAction(IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_ACTION,
MessageArgument.trusted("bar", bar.toString()),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("generated", Form.f(generated)),
MessageArgument.trusted("required", Form.f(required))
));
} else if (surface == HudSurface.BOSS_BAR) {
BukkitPlatform.hudLanes().show(sender.player(), "iris:world-create", IrisLanguage.text(
RuntimeProgressMessages.WORLD_CREATE_ACTION,
MessageArgument.trusted("bar", ""),
MessageArgument.trusted("percent", percent),
MessageArgument.trusted("generated", Form.f(generated)),
MessageArgument.trusted("required", Form.f(required))
), progress, BarColor.GREEN, BarStyle.SOLID, 4000L);
}
return;
}
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)); }, interval));
}); });
return taskId; 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); AtomicInteger taskId = new AtomicInteger(-1);
AtomicLong lastResolveMs = new AtomicLong(0L);
int interval = sender.isPlayer() ? 1 : 20; int interval = sender.isPlayer() ? 1 : 20;
taskId.set(J.ar(() -> { taskId.set(J.ar(() -> {
if (done.get()) { if (done.get()) {
@@ -597,10 +612,16 @@ public class IrisCreator {
double p = progress.get(); double p = progress.get();
int percent = (int) Math.round(p * 100.0D); int percent = (int) Math.round(p * 100.0D);
if (sender.isPlayer() && claim != null) { if (creationReporter != null) {
HudSurface surface = resolveThrottled(claim, lastResolveMs); creationReporter.update(0.94D + (0.05D * p), "pregenerate");
if (surface == HudSurface.ACTION_BAR) { return;
BukkitPlatform.hudLanes().hide(sender.player(), "iris:pregen"); }
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 barWidth = 44;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth); int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth);
StringBuilder bar = new StringBuilder(barWidth * 3 + 4); StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
@@ -614,13 +635,6 @@ public class IrisCreator {
MessageArgument.trusted("bar", bar.toString()), MessageArgument.trusted("bar", bar.toString()),
MessageArgument.trusted("percent", percent) 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; return;
} }
@@ -632,31 +646,12 @@ public class IrisCreator {
return taskId; return taskId;
} }
private HudSlotClaim openLoaderClaim(String purpose) { private void hidePregenLane(boolean shown) {
return BukkitPlatform.hudSlots().open(sender.player(), new HudSlotRequest( if (!shown) {
purpose,
HudPriority.PROGRESS,
1200L,
List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)
));
}
private void releaseLoaderClaim(HudSlotClaim claim, String laneId) {
if (claim == null) {
return; return;
} }
claim.release(); BukkitPlatform.hudLanes().hide(sender.player(), "iris:pregen");
BukkitPlatform.hudLanes().hide(sender.player(), laneId); sender.sendAction(" ");
}
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;
} }
private void cancelRepeatingTask(AtomicInteger taskId) { 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 SEED_OFFSET = 29356788L;
private static final long PART_FACTOR = 10439677L; 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 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() { private static boolean detectBukkit() {
try { try {
@@ -220,6 +228,7 @@ final class DecoratorCore {
return; return;
} }
block = stackedVineBlock(block, stack, 0);
data.set(x, targetY, z, block); data.set(x, targetY, z, block);
return; return;
} }
@@ -259,6 +268,7 @@ final class DecoratorCore {
bd = dripstoneBlock(stack, i, "up"); bd = dripstoneBlock(stack, i, "up");
} }
bd = stackedVineBlock(bd, stack, i);
data.set(x, height + 1 + i, z, bd); data.set(x, height + 1 + i, z, bd);
} }
} }
@@ -280,6 +290,7 @@ final class DecoratorCore {
if (block == null) { if (block == null) {
return; return;
} }
block = stackedVineBlock(block, stack, 0);
data.set(x, height, z, fixFacesForHunk(block, data, x, z, realX, height, realZ, mantle)); data.set(x, height, z, fixFacesForHunk(block, data, x, z, realX, height, realZ, mantle));
return; return;
} }
@@ -303,6 +314,7 @@ final class DecoratorCore {
bd = dripstoneBlock(stack, i, "down"); bd = dripstoneBlock(stack, i, "down");
} }
bd = stackedVineBlock(bd, stack, i);
if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) { if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) {
break; break;
} }
@@ -367,6 +379,7 @@ final class DecoratorCore {
if (bd == null) { if (bd == null) {
break; break;
} }
bd = stackedVineBlock(bd, stack, i);
data.set(xf, h, zf, bd); data.set(xf, h, zf, bd);
placed++; placed++;
} }
@@ -431,6 +444,9 @@ final class DecoratorCore {
} }
static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) { static boolean canGoOn(PlatformBlockState decorator, PlatformBlockState surface) {
if (!B.canPlaceOnto(decorator, surface)) {
return false;
}
if (!BUKKIT_PRESENT) { if (!BUKKIT_PRESENT) {
DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness(); DecoratorPlatformHooks.SurfaceSturdiness sturdiness = DecoratorPlatformHooks.surfaceSturdiness();
return sturdiness == null ? B.isSolid(surface) : sturdiness.canGoOn(surface); 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); 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 return surface != null
&& B.isSolid(surface) && B.isSolid(surface)
&& (decorator.isForcePlace() || canGoOn(null, surface)); && (decorator.isForcePlace() || canGoOn(decorant, surface));
} }
static boolean canReplaceStackTarget(PlatformBlockState state, boolean allowFluid) { static boolean canReplaceStackTarget(PlatformBlockState state, boolean allowFluid) {
@@ -503,4 +519,28 @@ final class DecoratorCore {
} }
return dripstoneDown[thIdx]; 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; return;
} }
if (!DecoratorCore.isValidShorelineSupport(decorator, data.get(x, height, z))) { PlatformBlockState support = data.get(x, height, z);
if (support == null || !support.isSolid()) {
return; return;
} }
@@ -76,7 +77,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return; return;
} }
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData()); 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); data.set(x, targetY, z, block);
} }
return; return;
@@ -96,7 +97,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
} }
PlatformBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()); 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); data.set(x, targetY, z, block);
} }
return; return;
@@ -116,6 +117,9 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
if (block == null) { if (block == null) {
break; break;
} }
if (i == 0 && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
break;
}
data.set(x, targetY, z, block); 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.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; 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.Sound;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; 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.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent; import org.bukkit.event.world.WorldSaveEvent;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -98,12 +93,8 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
runManagerTask("bukkit_world_manager_hotload_event", () -> { runManagerTask("bukkit_world_manager_hotload_event", () -> {
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) { for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f); 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); 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) { if (closed) {
return; return;
} }
flushDirty(); if (!flushDirty()) {
throw new IllegalStateException(
"Cannot close native structure ownership storage while Mantle regions are in use");
}
closed = true; closed = true;
authorities.invalidateAll(); authorities.invalidateAll();
} finally { } finally {
@@ -177,12 +180,15 @@ public final class NativeStructureOwnershipStore {
} }
} }
private void flushDirty() { private boolean flushDirty() {
if (!dirty) { if (!dirty) {
return; return true;
} }
storage.flush(); if (storage.flush()) {
dirty = false; dirty = false;
return true;
}
return false;
} }
private Authority loadAuthority( private Authority loadAuthority(
@@ -217,7 +223,7 @@ public final class NativeStructureOwnershipStore {
void remove(long target, String structureKey, int originChunkX, int originChunkZ); void remove(long target, String structureKey, int originChunkX, int originChunkZ);
void flush(); boolean flush();
} }
private static final class MantleStorage implements Storage { private static final class MantleStorage implements Storage {
@@ -296,10 +302,10 @@ public final class NativeStructureOwnershipStore {
} }
@Override @Override
public void flush() { public boolean flush() {
Map<Long, NativeStructureOwnershipBundle> pending = new TreeMap<>(pendingBundles); Map<Long, NativeStructureOwnershipBundle> pending = new TreeMap<>(pendingBundles);
if (pending.isEmpty()) { if (pending.isEmpty()) {
return; return true;
} }
Mantle<Matter> mantle = mantle(); Mantle<Matter> mantle = mantle();
@@ -309,11 +315,16 @@ public final class NativeStructureOwnershipStore {
restorePendingBundle(mantle, target, entry.getValue()); restorePendingBundle(mantle, target, entry.getValue());
regions.add(Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5)); 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()) { 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, private void restorePendingBundle(Mantle<Matter> mantle, long target,
NativeStructureOwnershipBundle bundle) { NativeStructureOwnershipBundle bundle) {
@@ -412,7 +412,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
CaveAnchorCache caveAnchorCache = new CaveAnchorCache(); CaveAnchorCache caveAnchorCache = new CaveAnchorCache();
for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) { for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) {
boolean treePlacement = p instanceof IrisProceduralTree; 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) { if (golden) {
IrisLogging.info("Goldendebug procedural chance: chunk=" + x + "," + z IrisLogging.info("Goldendebug procedural chance: chunk=" + x + "," + z
+ " scope=" + scope + " 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( private CavePlacementAnchor findCavePlacementAnchor(
MantleWriter writer, MantleWriter writer,
RNG rng, 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.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; 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.project.context.ChunkContext;
import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
@@ -79,7 +80,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
CarveWallBuffer walls = scratch.walls; CarveWallBuffer walls = scratch.walls;
CarveColumnMask[] columnMasks = scratch.columnMasks; CarveColumnMask[] columnMasks = scratch.columnMasks;
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks; CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
MatterCavern[] boundaryCaverns = scratch.boundaryCaverns;
int[] surfaceHeights = scratch.surfaceHeights; int[] surfaceHeights = scratch.surfaceHeights;
Map<String, IrisBiome> customBiomeCache = scratch.customBiomeCache; Map<String, IrisBiome> customBiomeCache = scratch.customBiomeCache;
UpperDimensionContext upperCtx = getEngine().getUpperContext(); UpperDimensionContext upperCtx = getEngine().getUpperContext();
@@ -155,7 +155,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} else { } else {
addInternalWallsFromMasks(walls, columnMasks); 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()); getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
@@ -179,18 +179,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}); });
for (int columnIndex = 0; columnIndex < 256; columnIndex++) { 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++) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) { if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
continue; continue;
} }
MatterCavern cavern = boundaryCaverns[columnIndex]; processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], walls, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
if (cavern == null) {
continue;
}
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], cavern, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
} }
// Surface-break carving must not leave an ore cap suspended across the opening. // 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, MantleChunk<Matter> mc,
CarveWallBuffer walls, CarveWallBuffer walls,
CarveColumnMask[] boundaryMasks, CarveColumnMask[] boundaryMasks,
MatterCavern[] boundaryCaverns,
int chunkX, int chunkX,
int chunkZ, int chunkZ,
int[] surfaceHeights int[] surfaceHeights
@@ -349,16 +344,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
for (int yy = 1; yy <= maxY; yy++) { for (int yy = 1; yy <= maxY; yy++) {
for (int offset = 0; offset < 16; offset++) { for (int offset = 0; offset < 16; offset++) {
if (west != null) { 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) { 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) { 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) { 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, MantleChunk<Matter> neighborChunk,
CarveWallBuffer walls, CarveWallBuffer walls,
CarveColumnMask[] boundaryMasks, CarveColumnMask[] boundaryMasks,
MatterCavern[] boundaryCaverns,
int localX, int localX,
int yy, int yy,
int localZ, int localZ,
@@ -388,9 +382,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
walls.put(localX, yy, localZ, neighbor); walls.put(localX, yy, localZ, neighbor);
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
boundaryMasks[columnIndex].add(yy); boundaryMasks[columnIndex].add(yy);
if (boundaryCaverns[columnIndex] == null) {
boundaryCaverns[columnIndex] = neighbor;
}
} }
private MantleChunk<Matter> existingMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) { private MantleChunk<Matter> existingMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
@@ -410,7 +401,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
int chunkX, int chunkX,
int chunkZ, int chunkZ,
IrisDimensionCarvingResolver.State resolverState, IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache
) { ) {
if (columnMask == null || columnMask.isEmpty()) { if (columnMask == null || columnMask.isEmpty()) {
return; return;
@@ -437,7 +429,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
zone.ceiling = buf; zone.ceiling = buf;
} else { } else {
if (zone.isValid(getEngine())) { 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 = new CaveZone();
zone.setFloor(y); zone.setFloor(y);
@@ -449,14 +441,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
} }
if (zone.isValid(getEngine())) { 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( private void processBoundaryColumnFromMask(
Hunk<PlatformBlockState> output, Hunk<PlatformBlockState> output,
CarveColumnMask boundaryMask, CarveColumnMask boundaryMask,
MatterCavern cavern, CarveWallBuffer walls,
int columnIndex, int columnIndex,
int chunkX, int chunkX,
int chunkZ, int chunkZ,
@@ -481,19 +473,19 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (y == zoneCeiling + 1) { if (y == zoneCeiling + 1) {
zoneCeiling = y; zoneCeiling = y;
} else { } 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; zoneFloor = y;
zoneCeiling = y; zoneCeiling = y;
} }
y = boundaryMask.nextSetBit(y + 1); 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( private void paintBoundaryZone(
Hunk<PlatformBlockState> output, Hunk<PlatformBlockState> output,
MatterCavern cavern, CarveWallBuffer walls,
int rx, int rx,
int rz, int rz,
int worldX, int worldX,
@@ -504,62 +496,60 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache Map<String, IrisBiome> customBiomeCache
) { ) {
int center = (zoneFloor + zoneCeiling) / 2; IrisBiome floorBiome = resolveCaveBoundaryBiome(
String customBiome = cavern.getCustomBiome(); walls.get(rx, zoneFloor, rz), worldX, zoneFloor, worldZ,
IrisBiome biome = customBiome.isEmpty() resolverState, caveBiomeCache, customBiomeCache);
? resolveCaveBiome(caveBiomeCache, worldX, center, worldZ, resolverState) IrisBiome ceilingBiome = resolveCaveBoundaryBiome(
: resolveCustomBiome(customBiomeCache, customBiome); walls.get(rx, zoneCeiling, rz), worldX, zoneCeiling, worldZ,
resolverState, caveBiomeCache, customBiomeCache);
if (biome == null) { if (floorBiome == null && ceilingBiome == null) {
return; return;
} }
if (floorBiome != null) {
KList<PlatformBlockState> floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex()); KList<PlatformBlockState> floorLayers = floorBiome.generateLayers(
getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
for (int i = 0; i < zoneFloor - 1; i++) { for (int i = 0; i < zoneFloor - 1; i++) {
if (!floorLayers.hasIndex(i)) { if (!floorLayers.hasIndex(i)) {
break; break;
} }
int floorY = zoneFloor - i - 1;
int fy = zoneFloor - i - 1; if (floorY < 0) {
if (fy < 0) {
break; break;
} }
PlatformBlockState existing = output.getRaw(rx, floorY, rz);
PlatformBlockState down = output.getRaw(rx, fy, rz);
if (!B.isSolid(down)) {
break;
}
PlatformBlockState layer = floorLayers.get(i); PlatformBlockState layer = floorLayers.get(i);
if (B.isOre(down)) { if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, floorY, rz, layer)) {
output.setRaw(rx, fy, rz, B.toDeepSlateOre(down, layer));
continue; continue;
} }
if (B.isOre(existing)) {
output.setRaw(rx, fy, rz, layer); 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(); 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++) { for (int i = 0; i < ceilingLayers.size(); i++) {
int cy = zoneCeiling + i + 1; int ceilingY = zoneCeiling + i + 1;
if (cy >= worldMaxY) { if (ceilingY >= worldMaxY) {
break; break;
} }
PlatformBlockState existing = output.getRaw(rx, ceilingY, rz);
PlatformBlockState up = output.getRaw(rx, cy, rz); if (!B.isSolid(existing)) {
if (!B.isSolid(up)) {
continue; continue;
} }
PlatformBlockState layer = ceilingLayers.get(i); PlatformBlockState layer = ceilingLayers.get(i);
if (B.isOre(up)) { if (B.isOre(existing)) {
output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, layer)); output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer));
continue; continue;
} }
output.setRaw(rx, ceilingY, rz, layer);
output.setRaw(rx, cy, rz, layer); }
} }
} }
@@ -575,10 +565,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return (h & 15L) == 0L; 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) { 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 center = (zone.floor + zone.ceiling) / 2;
int maxY = output.getHeight(); int maxY = output.getHeight();
String customBiome = "";
if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) { if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) {
output.setRaw(rx, zone.ceiling + 1, rz, AIR); 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); mantle.set(xx, zone.floor, zz, MarkerMatter.CAVE_FLOOR);
} }
for (int i = zone.floor; i <= zone.ceiling; i++) { IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
MatterCavern cavernData = (MatterCavern) mc.getOrCreate(PowerOfTwoCoordinates.floorDivPow2(i, 4)).slice(MatterCavern.class) IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
.get(rx, i & 15, rz); if (floorBiome == null && ceilingBiome == null) {
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) {
return; return;
} }
if (floorBiome != null) {
KList<PlatformBlockState> blocks = biome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex()); KList<PlatformBlockState> floorBlocks = floorBiome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
for (int i = 0; i < zone.floor - 1; i++) { for (int i = 0; i < zone.floor - 1; i++) {
if (!blocks.hasIndex(i)) { if (!floorBlocks.hasIndex(i)) {
break; break;
} }
int y = zone.floor - i - 1; int y = zone.floor - i - 1;
PlatformBlockState block = floorBlocks.get(i);
PlatformBlockState b = blocks.get(i); PlatformBlockState existing = output.getRaw(rx, y, rz);
PlatformBlockState down = output.getRaw(rx, y, rz); if (!B.isSolid(existing) || !canReplaceCaveFloorLayer(output, rx, y, rz, block)) {
if (!B.isSolid(down)) {
continue; continue;
} }
if (B.isOre(existing)) {
if (B.isOre(down)) { output.setRaw(rx, y, rz, B.toDeepSlateOre(existing, block));
output.setRaw(rx, y, rz, B.toDeepSlateOre(down, b));
continue; continue;
} }
output.setRaw(rx, y, rz, block);
output.setRaw(rx, y, rz, blocks.get(i)); }
} }
blocks = biome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex()); if (ceilingBiome != null) {
KList<PlatformBlockState> ceilingBlocks = ceilingBiome.generateCeilingLayers(getDimension(), xx, zz, rng, 3, zone.ceiling, getData(), getComplex());
for (int i = 0; i < blocks.size(); i++) { for (int i = 0; i < ceilingBlocks.size(); i++) {
int cy = zone.ceiling + i + 1; int cy = zone.ceiling + i + 1;
if (cy >= maxY) { if (cy >= maxY) {
break; break;
} }
PlatformBlockState block = ceilingBlocks.get(i);
PlatformBlockState b = blocks.get(i); PlatformBlockState existing = output.getRaw(rx, cy, rz);
PlatformBlockState up = output.getRaw(rx, cy, rz); if (!B.isSolid(existing)) {
if (!B.isSolid(up)) {
continue; continue;
} }
if (B.isOre(existing)) {
if (B.isOre(up)) { output.setRaw(rx, cy, rz, B.toDeepSlateOre(existing, block));
output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, b));
continue; continue;
} }
output.setRaw(rx, cy, rz, block);
output.setRaw(rx, cy, rz, b); }
} }
IrisDecorator[] surfaceDecorators = biome.getDecoratorBucket(IrisDecorationPart.NONE); IrisDecorator[] surfaceDecorators = floorBiome == null
if (surfaceDecorators.length > 0 && zone.getFloor() > 0 && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) { ? new IrisDecorator[0]
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness()); : 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))) { 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) { private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState); IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState);
if (center == null) { if (center == null) {
@@ -29,7 +29,6 @@ import java.util.Map;
final class IrisCarveScratch { final class IrisCarveScratch {
final CarveColumnMask[] columnMasks = new CarveColumnMask[256]; final CarveColumnMask[] columnMasks = new CarveColumnMask[256];
final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256]; final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256];
final MatterCavern[] boundaryCaverns = new MatterCavern[256];
final int[] surfaceHeights = new int[256]; final int[] surfaceHeights = new int[256];
final CarveWallBuffer walls = new CarveWallBuffer(512); final CarveWallBuffer walls = new CarveWallBuffer(512);
final Map<String, IrisBiome> customBiomeCache = new HashMap<>(); final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
@@ -54,7 +53,6 @@ final class IrisCarveScratch {
for (int index = 0; index < columnMasks.length; index++) { for (int index = 0; index < columnMasks.length; index++) {
columnMasks[index].clear(); columnMasks[index].clear();
boundaryMasks[index].clear(); boundaryMasks[index].clear();
boundaryCaverns[index] = null;
} }
walls.clear(); walls.clear();
customBiomeCache.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) { void forEach(Consumer consumer) {
for (int index = 0; index < keys.length; index++) { for (int index = 0; index < keys.length; index++) {
int key = keys[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.framework.EngineAssignedModifier;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDepositGenerator; 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.IrisDepositVariant;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; 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 x = rng.i(min, max + 1);
int z = 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; continue;
int minY = Math.max(0, k.getMinHeight()); int biomeY = Math.max(0, Math.min(getEngine().getHeight() - 1, y));
int maxY = Math.min(height, Math.min(getEngine().getHeight() - 1, k.getMaxHeight())); boolean oreDeposit = k.isOre(getData());
IrisBiome surfaceBiome = context.getBiome().get(x, z);
if (minY >= maxY) IrisBiome depositBiome = oreDeposit || k.usesCaveBiomeFilter()
? getEngine().getCaveBiome(
(cx << 4) + x, biomeY, (cz << 4) + z, carvingState)
: null;
if (!k.matchesBiome(surfaceBiome, depositBiome)) {
continue; continue;
}
int y = rng.i(minY, maxY + 1); if (oreDeposit) {
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 (depositBiome != null) { if (depositBiome != null) {
double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier(); double frequencyMultiplier = depositBiome.getOreDepositFrequencyMultiplier();
if (frequencyMultiplier < 1D if (frequencyMultiplier < 1D
@@ -146,7 +160,9 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier); IrisObject scaledClump = k.getClump(getEngine(), rng, getData(), sizeMultiplier);
int scaledDimension = scaledClump.getW(); int scaledDimension = scaledClump.getW();
x = clampDepositCenter(x, scaledDimension, 16); x = clampDepositCenter(x, scaledDimension, 16);
if (clippedHeight) {
y = clampDepositCenter(y, scaledDimension, getEngine().getHeight()); y = clampDepositCenter(y, scaledDimension, getEngine().getHeight());
}
z = clampDepositCenter(z, scaledDimension, 16); z = clampDepositCenter(z, scaledDimension, 16);
clump = scaledClump; 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) { if (nx > 15 || nx < 0 || ny >= getEngine().getHeight() || ny < 0 || nz < 0 || nz > 15) {
continue; continue;
} }
int columnSurfaceLimit = getDepositSurfaceLimit(cx, cz, nx, nz, he, context); int columnSurface = getDepositTerrainSurface(cx, cz, nx, nz, he, context);
if (ny > columnSurfaceLimit) { if (!placementSurfaceAllows(
k.getPlacementScope(), ny, columnSurface, k.getSurfaceClearance())) {
continue; continue;
} }
@@ -172,9 +189,16 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
if (!canReplaceDepositTarget(current)) { if (!canReplaceDepositTarget(current)) {
continue; continue;
} }
if (!k.canReplace(current)) {
continue;
}
if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) { if (!k.isReplaceBedrock() && IrisProceduralBlocks.materialKey(current).equals("minecraft:bedrock")) {
continue; continue;
} }
if (shouldDiscardExposed(
k.getDiscardChanceOnAirExposure(), rng.d(), isAdjacentToAir(data, nx, ny, nz))) {
continue;
}
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) { if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
PlatformBlockState ore = clump.getBlocks().get(j); 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) { private int getDepositTerrainSurface(
int surfaceY = heightMap != null int cx, int cz, int localX, int localZ, HeightMap heightMap,
ChunkContext context) {
return heightMap != null
? heightMap.getHeight((cx << 4) + localX, (cz << 4) + localZ) ? heightMap.getHeight((cx << 4) + localX, (cz << 4) + localZ)
: context.getRoundedHeight(localX, localZ); : context.getRoundedHeight(localX, localZ);
return depositSurfaceLimit(surfaceY);
} }
static int depositSurfaceLimit(int 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) { static int absoluteWorldY(int minHeight, int localY) {
@@ -208,6 +247,54 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return state != null && !state.isAir() && !state.isFluid(); 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) { static boolean passesOreFrequency(double multiplier, double sample) {
return multiplier >= 1D || sample < Math.max(0D, multiplier); 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.") @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()); private KList<IrisBiomePaletteLayer> layers = new KList<IrisBiomePaletteLayer>().qadd(new IrisBiomePaletteLayer());
@ArrayType(type = IrisBiomePaletteLayer.class) @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.") @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<IrisBiomePaletteLayer>().qadd(new IrisBiomePaletteLayer()); private KList<IrisBiomePaletteLayer> caveCeilingLayers = new KList<>();
@ArrayType(type = IrisBiomePaletteLayer.class) @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.") @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<>(); private KList<IrisBiomePaletteLayer> seaLayers = new KList<>();
@@ -119,7 +119,7 @@ public class IrisDecorator {
return getHeightGenerator(rng, data) return getHeightGenerator(rng, data)
.fit(stackMin, stackMax, .fit(stackMin, stackMax,
x / heightVariance.getZoom(), x / heightVariance.getZoom(),
z / heightVariance.getZoom()) + 1; z / heightVariance.getZoom());
} }
public CNG getHeightGenerator(RNG rng, IrisData data) { 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.Required;
import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.spi.PlatformBlockState; 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.IrisBlockVector;
import art.arcane.iris.util.common.math.Vector3i; import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.collection.KList; 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 ConcurrentMap<ClumpCacheKey, KList<IrisObject>> objects = new ConcurrentHashMap<>();
private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>(); private final transient AtomicCache<KList<PlatformBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<Boolean> ore = 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<>(); private final transient ConcurrentMap<ClumpCacheKey, KList<IrisObject>> scaledObjects = new ConcurrentHashMap<>();
@Required @Required
@MinNumber(0) @MinNumber(-8192)
@MaxNumber(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; private int minHeight = 1;
@Required @Required
@MinNumber(0) @MinNumber(-8192)
@MaxNumber(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; 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 @Required
@MinNumber(0) @MinNumber(0)
@MaxNumber(8192) @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; private int minSize = 0;
@Required @Required
@MinNumber(0) @MinNumber(0)
@MaxNumber(8192) @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; 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 @Required
@MinNumber(0) @MinNumber(0)
@MaxNumber(2048) @MaxNumber(2048)
@@ -90,6 +102,10 @@ public class IrisDepositGenerator {
@MaxNumber(1) @MaxNumber(1)
@Desc("The chance of each individual clump spawning in a chunk") @Desc("The chance of each individual clump spawning in a chunk")
private double perClumpSpawnChance = 1; 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 @Required
@ArrayType(min = 1, type = IrisBlockData.class) @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.") @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) @MaxNumber(64)
@Desc("Ore varience is how many different objects clumps iris will create") @Desc("Ore varience is how many different objects clumps iris will create")
private int varience = 3; 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") @Desc("If set to true, this deposit will replace bedrock")
private boolean replaceBedrock = false; private boolean replaceBedrock = false;
public IrisObject getClump(Engine engine, RNG rng, IrisData rdata) { 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); ClumpCacheKey cacheKey = new ClumpCacheKey(engine.getSeedManager().getDeposit(), minSize, maxSize);
KList<IrisObject> objects = this.objects.computeIfAbsent(cacheKey, key -> { KList<IrisObject> objects = this.objects.computeIfAbsent(cacheKey, key -> {
RNG rngv = new RNG(key.depositSeed() + hashCode()); RNG rngv = new RNG(key.depositSeed() + hashCode());
@@ -124,6 +155,10 @@ public class IrisDepositGenerator {
int scaledMinSize = scaledDepositSize(minSize, sizeMultiplier); int scaledMinSize = scaledDepositSize(minSize, sizeMultiplier);
int scaledMaxSize = scaledDepositSize(maxSize, sizeMultiplier); int scaledMaxSize = scaledDepositSize(maxSize, sizeMultiplier);
if (shape != IrisDepositShape.IRIS) {
return generateConfiguredClumpObject(rng, rdata, scaledMinSize, scaledMaxSize);
}
ClumpCacheKey cacheKey = new ClumpCacheKey( ClumpCacheKey cacheKey = new ClumpCacheKey(
engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize); engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize);
KList<IrisObject> objects = scaledObjects.computeIfAbsent(cacheKey, key -> { 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))); 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) { private IrisObject generateClumpObject(RNG rngv, IrisData rdata, int clumpMinSize, int clumpMaxSize) {
int s = rngv.i(clumpMinSize, clumpMaxSize + 1); int s = rngv.i(clumpMinSize, clumpMaxSize + 1);
if (s == 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) { 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) @Accessors(chain = true)
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @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 @Data
public class IrisFormation implements IrisProceduralPlacement { public class IrisFormation implements IrisProceduralPlacement {
private final transient AtomicCache<KList<IrisObject>> variantCache = new AtomicCache<>(); 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.") @Desc("The thickness in blocks of the ARCH legs and spanning curve.")
private int archThickness = 3; 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) @MinNumber(2)
@MaxNumber(12) @MaxNumber(12)
@Desc("How many separate columns make up a BASALT_COLUMN cluster. Each column gets a randomized height around the formation height range.") @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).") @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; 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) { public KList<IrisObject> getVariantObjects(IrisData data) {
return variantCache.aquire(() -> { return variantCache.aquire(() -> {
KList<IrisObject> baked = new KList<>(); KList<IrisObject> baked = new KList<>();
@@ -20,7 +20,7 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc; 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 { 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).") @Desc("A tall, slender needle of rock that tapers smoothly to a point at the top (a sharp pinnacle or stone spire).")
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.") @Desc("A rounded, lumpy boulder formed from a noise-perturbed ellipsoid that sits low on the terrain.")
BOULDER, BOULDER,
@Desc("A tightly packed cluster of several vertical, near-hexagonal columns of varying height (a basalt column formation / giant's causeway).") @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; return false;
} }
int envelopeMinY = paint ? Integer.MIN_VALUE : y - margin; int worldY = y + engine.getMinHeight();
int envelopeMaxY = paint ? Integer.MAX_VALUE : y + margin; int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin;
boolean envelopeMeetsPiece = false; boolean envelopeMeetsPiece = false;
for (NativeStructureVolume volume : volumes) { for (NativeStructureVolume volume : volumes) {
if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) { 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) ? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2)
+ placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater()) + placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater())
: y + (int) Math.round(i.getY()); : y + (int) Math.round(i.getY());
int worldBlockY = yy + engine.getMinHeight();
for (NativeStructureVolume volume : volumes) { for (NativeStructureVolume volume : volumes) {
if (volume.containsWithin(xx, yy, zz, warpMargin)) { if (volume.containsWithin(xx, worldBlockY, zz, warpMargin)) {
return true; return true;
} }
} }
@@ -51,6 +51,10 @@ public final class FormationGenerator {
case SEA_STACK -> FormationShapeBuilder.seaStack(canvas, f, height, baseRadius, rng); case SEA_STACK -> FormationShapeBuilder.seaStack(canvas, f, height, baseRadius, rng);
case BOULDER -> FormationShapeBuilder.boulder(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 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()) { 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.IrisFormation;
import art.arcane.iris.engine.object.tree.TreeFunctions; import art.arcane.iris.engine.object.tree.TreeFunctions;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.math.RNG;
import java.util.HashMap;
import java.util.Map;
public final class FormationShapeBuilder { public final class FormationShapeBuilder {
private 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) { public static void spire(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) {
double topRadius = f.getTopWidth(); double topRadius = f.getTopWidth();
column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, true); 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) { 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) { public static void arch(FormationCanvas canvas, IrisFormation f, int height, double baseRadius, RNG rng) {
int span = Math.max(2, f.getArchSpan()); double span = Math.max(2.0, f.getArchSpan());
int thickness = Math.max(1, f.getArchThickness()); double tubeRadius = Math.max(0.75, f.getArchThickness() * 0.5);
int legHeight = Math.max(2, (int) Math.round(height * 0.55)); double asymmetry = Math.max(0.0, Math.min(1.0, f.getArchAsymmetry()));
int halfSpan = span / 2; double leftFootX = -span * 0.5 - tubeRadius - rng.d(0.0, span * 0.16) * asymmetry;
double legRadius = Math.max(1.0, thickness / 2.0 + baseRadius * 0.25); double rightFootX = span * 0.5 + tubeRadius + rng.d(0.0, span * 0.16) * asymmetry;
long noiseSeed = f.getSeed() + 7777L; double leftFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry;
double roughness = Math.max(0.0, Math.min(1.0, f.getRoughness())); 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) { ball(canvas, f, (int) Math.round(leftFootX), (int) Math.round(footRadius),
int legX = side * (halfSpan + (int) Math.ceil(legRadius)); (int) Math.round(leftFootZ), footRadius, false, noiseSeed, rng);
for (int y = 0; y < legHeight; y++) { ball(canvas, f, (int) Math.round(rightFootX), (int) Math.round(footRadius),
disc(canvas, f, legX, y, 0, legRadius, false, rng); (int) Math.round(rightFootZ), footRadius, false, noiseSeed, rng);
}
}
int archTop = legHeight + (int) Math.round(span * 0.45); for (int step = 0; step <= steps; step++) {
int leftX = -(halfSpan + (int) Math.ceil(legRadius)); double t = step / (double) steps;
int rightX = halfSpan + (int) Math.ceil(legRadius); double normalizedRise;
double archWidth = (rightX - leftX) / 2.0; double steepness;
double centerX = (leftX + rightX) / 2.0; if (t <= crownPosition) {
double archHeight = archTop - legHeight; normalizedRise = Math.sin((t / crownPosition) * Math.PI * 0.5);
int half = Math.max(1, thickness / 2); steepness = leftSteepness;
} else {
for (int x = leftX; x <= rightX; x++) { normalizedRise = Math.sin(((1.0 - t) / (1.0 - crownPosition)) * Math.PI * 0.5);
double nx = (x - centerX) / archWidth; steepness = rightSteepness;
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);
}
} }
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) { 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 lean = Math.toRadians(f.getLean());
double azimuth = Math.toRadians(f.getLeanAzimuth()); 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) { 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; int r = (int) Math.ceil(radius) + 1;
for (int x = -r; x <= r; x++) { 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.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.collection.KMap; 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.HudBossBarLane;
import art.arcane.volmlib.util.hud.HudSlotService;
import art.arcane.volmlib.util.math.Vector3d; import art.arcane.volmlib.util.math.Vector3d;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
@@ -65,7 +65,7 @@ import java.util.function.Supplier;
public final class BukkitPlatform implements IrisPlatform { public final class BukkitPlatform implements IrisPlatform {
private static volatile Plugin PLUGIN; private static volatile Plugin PLUGIN;
private static volatile Bindings.Adventure AUDIENCES; 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 HudBossBarLane HUD_LANES;
private static volatile Supplier<VolmitSender> CONSOLE; private static volatile Supplier<VolmitSender> CONSOLE;
private static volatile HostBridge BRIDGE; private static volatile HostBridge BRIDGE;
@@ -135,21 +135,21 @@ public final class BukkitPlatform implements IrisPlatform {
return adventure; return adventure;
} }
public static void hostHud(HudSlotService hudSlots, HudBossBarLane hudLanes) { public static void hostHud(HudActionBar hudBar, HudBossBarLane hudLanes) {
HUD_SLOTS = hudSlots; HUD_BAR = hudBar;
HUD_LANES = hudLanes; HUD_LANES = hudLanes;
} }
public static boolean hasHud() { public static boolean hasHud() {
return HUD_SLOTS != null && HUD_LANES != null; return HUD_BAR != null && HUD_LANES != null;
} }
public static HudSlotService hudSlots() { public static HudActionBar hudBar() {
HudSlotService hudSlots = HUD_SLOTS; HudActionBar hudBar = HUD_BAR;
if (hudSlots == null) { if (hudBar == null) {
throw new IllegalStateException("No Iris HUD slot service is hosted"); throw new IllegalStateException("No Iris HUD action bar is hosted");
} }
return hudSlots; return hudBar;
} }
public static HudBossBarLane hudLanes() { 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.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.volmlib.util.collection.KList; 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.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@@ -47,9 +48,7 @@ import org.bukkit.plugin.Plugin;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Represents a volume sender. A command sender with extra crap in it * Represents a volume sender. A command sender with extra crap in it
@@ -221,60 +220,37 @@ public class VolmitSender implements CommandSender {
s.sendMessage("========================================================"); s.sendMessage("========================================================");
} }
public void sendTitle(String title, String subtitle, int i, int s, int o) { public void sendProgress(double percent, String thing) {
try {
player().sendTitle(
LegacyComponentSerializer.legacySection().serialize(createComponent(title)),
LegacyComponentSerializer.legacySection().serialize(createComponent(subtitle)),
i / 50, s / 50, o / 50);
} catch (Throwable ignored) {
}
}
public void sendProgress(double percent, String thing, HudSurface titleSurface, HudSurface barSurface) {
if (percent < 0) {
int l = 44; int l = 44;
int g = (int) (1D * l); int g = (int) ((percent < 0 ? 1D : percent) * l);
if (titleSurface == HudSurface.TITLE) {
sendTitle(C.IRIS + thing + " ", 0, 500, 250);
}
if (barSurface == HudSurface.ACTION_BAR) {
sendActionNoProcessing("" + "" + pulse("#00ff80", "#00373d", 1D) + "<underlined> " + Form.repeat(" ", g) + "<reset>" + Form.repeat(" ", l - g)); 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) { public void sendAction(String action) {
try { try {
player().spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(action)))); deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(action)));
} catch (Throwable ignored) { } catch (Throwable ignored) {
} }
} }
public void sendActionNoProcessing(String action) { public void sendActionNoProcessing(String action) {
try { try {
player().spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponentNoProcessing(action)))); deliverAction(LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponentNoProcessing(action)));
} catch (Throwable ignored) { } catch (Throwable ignored) {
} }
} }
public void sendTitle(String subtitle, int i, int s, int o) { private void deliverAction(String legacy) {
try { Player player = player();
player().sendTitle( if (BukkitPlatform.hasHud()) {
" ", if (legacy.isBlank()) {
LegacyComponentSerializer.legacySection().serialize(createNoPrefixComponent(subtitle)), BukkitPlatform.hudBar().clear(player, "iris:action");
i / 50, s / 50, o / 50); } else {
} catch (Throwable ignored) { 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) { private Component createNoPrefixComponent(String message) {
@@ -313,22 +289,6 @@ public class VolmitSender implements CommandSender {
return MiniMessage.miniMessage().deserialize(C.mini(t)); 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 @Override
public void sendMessage(String message) { public void sendMessage(String message) {
if ((!IrisSettings.get().getGeneral().isUseCustomColorsIngame() && s instanceof Player) || !IrisSettings.get().getGeneral().isUseConsoleCustomColors()) { 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.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form; 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.localization.MessageArgument;
import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
public interface Job { public interface Job {
String getName(); String getName();
@@ -73,40 +67,18 @@ public interface Job {
default void execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) { default void execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) {
PrecisionStopwatch p = PrecisionStopwatch.start(); PrecisionStopwatch p = PrecisionStopwatch.start();
CompletableFuture<?> f = J.afut(this::execute); 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(() -> { int c = J.ar(() -> {
if (sender.isPlayer()) { if (sender.isPlayer()) {
long now = System.currentTimeMillis(); sender.sendProgress(getProgress(), getName());
if (now - lastResolveMs.get() >= 250L) {
lastResolveMs.set(now);
titleClaim.resolve();
barClaim.resolve();
}
HudSurface titleSurface = titleClaim.granted();
HudSurface barSurface = barClaim.granted();
sender.sendProgress(getProgress(), getName(), titleSurface, barSurface);
if (barSurface == HudSurface.BOSS_BAR) {
BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L); BukkitPlatform.showProgressLane(sender.player(), "iris:job", getName() + " " + getProgressString(), getProgress(), 4000L);
} else if (barSurface == HudSurface.ACTION_BAR) {
BukkitPlatform.hudLanes().hide(sender.player(), "iris:job");
}
} else { } else {
sender.sendMessage(getName() + ": " + getProgressString()); sender.sendMessage(getName() + ": " + getProgressString());
} }
}, sender.isPlayer() ? 0 : 20); }, sender.isPlayer() ? 0 : 20);
f.whenComplete((fs, ff) -> { f.whenComplete((fs, ff) -> {
J.car(c); J.car(c);
if (titleClaim != null) { if (sender.isPlayer()) {
titleClaim.release(); sender.sendAction(" ");
}
if (barClaim != null) {
barClaim.release();
BukkitPlatform.hudLanes().hide(sender.player(), "iris:job"); BukkitPlatform.hudLanes().hide(sender.player(), "iris:job");
} }
if (!silentMsg) { 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.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.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.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.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.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!", "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.failed_convert": "§cKonvertierung fehlgeschlagen: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Konvertiert: {get} in {value}", "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.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.looking_package": "Suche nach Paket: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "{type}.iris im Ordner {WORKSPACENAME} gefunden", "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.", "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.finalize_open": "Abschluss",
"iris.runtime.studio.stage.cleanup": "Bereinigung", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Generierung §e{percent}%§7 {generated}/{required} Chunks§8 ({remaining} verbleibend)", "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.action": "{bar}§7 §e{percent}%§7 | §fVorgenerierung",
"iris.runtime.world_create.pregen.console": "§6Vorgenerierung §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Vorgenerierung §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Regenerieren", "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.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.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.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.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.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!", "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.failed_convert": "§cNo se pudo convertir: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Convertido: {get} en {value}", "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.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.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_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", "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.finalize_open": "Finalizando",
"iris.runtime.studio.stage.cleanup": "Limpiando", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Generando §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} restantes)", "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.action": "{bar}§7 §e{percent}%§7 | §fPregenerando",
"iris.runtime.world_create.pregen.console": "§6Pregenerando §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Pregenerando §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Regenerar", "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.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.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.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.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.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!", "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.failed_convert": "§cMuuntaminen epäonnistui: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Muunnettu: {get} sisään {value}", "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.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.looking_package": "Etsin pakettia: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "Löytyi {type}0, iiris {WORKSPACENAME} kansio", "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", "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.finalize_open": "Viimeistellään",
"iris.runtime.studio.stage.cleanup": "Siivotaan", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Luodaan §e{percent}%§7 {generated}/{required} chunkia§8 ({remaining} vasen)", "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.action": "{bar}§7 §e{percent}%§7 | §fEsituotanto",
"iris.runtime.world_create.pregen.console": "§6Esituotanto §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Esituotanto §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Regin", "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.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.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.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.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.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 !", "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.failed_convert": "§cImpossible de convertir : {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Converti : {get} dans {value}", "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.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.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_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", "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.finalize_open": "Finalisation",
"iris.runtime.studio.stage.cleanup": "Nettoyage", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Génération §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} restants)", "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.action": "{bar}§7 §e{percent}%§7 | §fPrégénération",
"iris.runtime.world_create.pregen.console": "§6Prégénération §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Prégénération §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Régénérer", "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.may_we_suggest_name_irisworld_instead_2": "§cאפשר להציע את השם \"IrisWorld\"במקום?",
"iris.bukkit.commandiris.that_folder_already_exists": "§cהתיקיה הזו כבר קיימת!", "iris.bukkit.commandiris.that_folder_already_exists": "§cהתיקיה הזו כבר קיימת!",
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eנסה: Overworld vanilla, שטוח, TheendXTry", "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.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.exception_raised_during_creation_see_console_more_details": "§cלמעטים שגדלו במהלך הבריאה. ראו את הקונסולה לפרטים נוספים.",
"iris.bukkit.commandiris.successfully_created_your_world": "§aיצרתם בהצלחה את עולמכם!", "iris.bukkit.commandiris.successfully_created_your_world": "§aיצרתם בהצלחה את עולמכם!",
@@ -1132,7 +1134,9 @@
"iris.bukkit.runtime.irisconverter.failed_convert": "§cנכשל להמיר: {name}", "iris.bukkit.runtime.irisconverter.failed_convert": "§cנכשל להמיר: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7המונחים: {get} פנימה {value}", "iris.bukkit.runtime.irisconverter.converted_3": "§7המונחים: {get} פנימה {value}",
"iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cכמה סנטימטרים לא הצליחו להמיר. בדוק את הקונסולה לפרטים", "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.looking_package": "מחפש חבילה: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "נמצאו {type}חרירי {WORKSPACENAME} התיקיה", "iris.bukkit.runtime.studiosvc.found_iris_folder": "נמצאו {type}חרירי {WORKSPACENAME} התיקיה",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "נמצאו {type} מימד {WORKSPACENAME} תיקיה. Repacking", "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.finalize_open": "מסיים",
"iris.runtime.studio.stage.cleanup": "מנקה", "iris.runtime.studio.stage.cleanup": "מנקה",
"iris.runtime.world_create.teleport_failed": "§eהעולם נברא, אך התקשורת האוטומטית נכשלה. נסו לנסות /iris teleport world={world}", "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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6ייצור §e{percent}%§7 {generated}/{required} צ'אנקים§8 ({remaining} שמאל)", "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.action": "{bar}§7 §e{percent}%§7 | §fהפקה מוקדמת",
"iris.runtime.world_create.pregen.console": "§6הפקה מוקדמת §e{percent}%", "iris.runtime.world_create.pregen.console": "§6הפקה מוקדמת §e{percent}%",
"iris.runtime.chunk_job.title.regen": "מחדש", "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.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.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.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.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.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!", "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.failed_convert": "§cConversione non riuscita: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Convertito: {get} nel {value}", "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.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.looking_package": "Ricerca del pacchetto: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "Trovato {type}.iris nella cartella {WORKSPACENAME}", "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.", "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.finalize_open": "Finalizzazione",
"iris.runtime.studio.stage.cleanup": "Pulizia", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Generazione §e{percent}%§7 {generated}/{required} chunk§8 ({remaining} rimanenti)", "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.action": "{bar}§7 §e{percent}%§7 | §fPregenerazione",
"iris.runtime.world_create.pregen.console": "§6Pregenerazione §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Pregenerazione §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Rigenera", "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.may_we_suggest_name_irisworld_instead_2": "§c代わりに \"IrisWorld\" という名前はいかがですか?",
"iris.bukkit.commandiris.that_folder_already_exists": "§cそのフォルダーはすでに存在します!", "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.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.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.exception_raised_during_creation_see_console_more_details": "§c作成中に例外が発生しました。詳細はコンソールを確認してください。",
"iris.bukkit.commandiris.successfully_created_your_world": "§aワールドを作成しました!", "iris.bukkit.commandiris.successfully_created_your_world": "§aワールドを作成しました!",
@@ -1132,7 +1134,9 @@
"iris.bukkit.runtime.irisconverter.failed_convert": "§c変換に失敗しました: {name}", "iris.bukkit.runtime.irisconverter.failed_convert": "§c変換に失敗しました: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7変換完了: {get}{value}", "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.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.looking_package": "パッケージを探します: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} フォルダーに {type}.iris が見つかりました", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} フォルダーに {type}.iris が見つかりました",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "{WORKSPACENAME} フォルダーに {type} ディメンションが見つかりました。再パッケージしています", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "{WORKSPACENAME} フォルダーに {type} ディメンションが見つかりました。再パッケージしています",
@@ -1203,8 +1207,28 @@
"iris.runtime.studio.stage.finalize_open": "完了処理中", "iris.runtime.studio.stage.finalize_open": "完了処理中",
"iris.runtime.studio.stage.cleanup": "クリーンアップ中", "iris.runtime.studio.stage.cleanup": "クリーンアップ中",
"iris.runtime.world_create.teleport_failed": "§eワールドを作成しましたが、自動テレポートに失敗しました。/iris teleport world={world} をお試しください", "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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6生成中 §e{percent}%§7 {generated}/{required} チャンク§8(残り {remaining}", "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.action": "{bar}§7 §e{percent}%§7 | §f事前生成中",
"iris.runtime.world_create.pregen.console": "§6事前生成中 §e{percent}%", "iris.runtime.world_create.pregen.console": "§6事前生成中 §e{percent}%",
"iris.runtime.chunk_job.title.regen": "再生成", "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.may_we_suggest_name_irisworld_instead_2": "§c우리는 이름 \"을 제안 할 수있다IrisWorld\" 대신?",
"iris.bukkit.commandiris.that_folder_already_exists": "§c그 폴더는 이미 존재합니다!", "iris.bukkit.commandiris.that_folder_already_exists": "§c그 폴더는 이미 존재합니다!",
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e중 하나를 시도: overworld, 바닐라, 플랫, theend", "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.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.exception_raised_during_creation_see_console_more_details": "§c창조 중 발생한 예외. 자세한 내용은 콘솔을 참조하십시오.",
"iris.bukkit.commandiris.successfully_created_your_world": "§a세상을 성공적으로 만들었습니다!", "iris.bukkit.commandiris.successfully_created_your_world": "§a세상을 성공적으로 만들었습니다!",
@@ -1132,7 +1134,9 @@
"iris.bukkit.runtime.irisconverter.failed_convert": "§c변환 실패: {name}", "iris.bukkit.runtime.irisconverter.failed_convert": "§c변환 실패: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7변환 완료: {get}, 소요 {value}", "iris.bukkit.runtime.irisconverter.converted_3": "§7변환 완료: {get}, 소요 {value}",
"iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c일부 설계도 변환 실패. 자세한 내용은 콘솔을 확인하십시오.", "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.looking_package": "포장을 위한 보기: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} 폴더에서 {type}.iris를 찾았습니다", "iris.bukkit.runtime.studiosvc.found_iris_folder": "{WORKSPACENAME} 폴더에서 {type}.iris를 찾았습니다",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "{WORKSPACENAME} 폴더에서 {type} 차원을 찾았습니다. 다시 패키징합니다", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "{WORKSPACENAME} 폴더에서 {type} 차원을 찾았습니다. 다시 패키징합니다",
@@ -1203,8 +1207,28 @@
"iris.runtime.studio.stage.finalize_open": "마무리 중", "iris.runtime.studio.stage.finalize_open": "마무리 중",
"iris.runtime.studio.stage.cleanup": "정리 중", "iris.runtime.studio.stage.cleanup": "정리 중",
"iris.runtime.world_create.teleport_failed": "§e월드를 만들었지만 자동 순간이동에 실패했습니다. /iris teleport world={world}을(를) 사용해 보세요", "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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6생성 중 §e{percent}%§7 {generated}/{required} 청크§8 ({remaining} 남음)", "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.action": "{bar}§7 §e{percent}%§7 | §f연구분야",
"iris.runtime.world_create.pregen.console": "§6연구분야 §e{percent}%", "iris.runtime.world_create.pregen.console": "§6연구분야 §e{percent}%",
"iris.runtime.chunk_job.title.regen": "재생성", "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.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.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.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.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.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į!", "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.failed_convert": "§cNepavyko konvertuoti: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Konvertuotas: {get} yra {value}", "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.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.looking_package": "Ieškoma paketo: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "Aplanke {WORKSPACENAME} rastas {type}.iris", "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", "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.finalize_open": "Užbaigiama",
"iris.runtime.studio.stage.cleanup": "Valoma", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Generuojama §e{percent}%§7 {generated}/{required} chunkai§8 ({remaining} kairėje)", "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.action": "{bar}§7 §e{percent}%§7 | §fRegeneravimas",
"iris.runtime.world_create.pregen.console": "§6Regeneravimas §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Regeneravimas §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Režen", "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.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.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.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.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.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!", "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.failed_convert": "§cConverteren mislukt: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Omgezet: {get} de {value}", "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.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.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_iris_folder": "Gevonden {type}Iris in {WORKSPACENAME} map",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Gevonden {type} dimensie in {WORKSPACENAME} map. Herverpakking", "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.finalize_open": "Afronden",
"iris.runtime.studio.stage.cleanup": "Opruimen", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Genereren §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} links)", "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.action": "{bar}§7 §e{percent}%§7 | §fPregeneren",
"iris.runtime.world_create.pregen.console": "§6Pregeneren §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Pregeneren §e{percent}%",
"iris.runtime.chunk_job.title.regen": "RegenName", "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.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.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.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.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.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!", "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.failed_convert": "§cNie można przekonwertować: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Przekonwertowany: {get} w {value}", "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.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.looking_package": "Szukam pakietu: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "Znaleziono {type}.iris w {WORKSPACENAME} katalog", "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", "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.finalize_open": "Finalizowanie",
"iris.runtime.studio.stage.cleanup": "Czyszczenie", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Generowanie §e{percent}%§7 {generated}/{required} chunki§8 ({remaining} lewa)", "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.action": "{bar}§7 §e{percent}%§7 | §fPregeneracja",
"iris.runtime.world_create.pregen.console": "§6Pregeneracja §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Pregeneracja §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Regen.", "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.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.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.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.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.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!", "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.failed_convert": "§cFalha ao converter: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Convertido: {get} em {value}", "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.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.looking_package": "Procurando pelo pacote: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "Encontrado {type}.iris em {WORKSPACENAME} pasta", "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", "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.finalize_open": "A finalizar",
"iris.runtime.studio.stage.cleanup": "A limpar", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Gerando §e{percent}%§7 {generated}/{required} chunks§8 ({remaining} esquerda)", "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.action": "{bar}§7 §e{percent}%§7 | §fPregeneração",
"iris.runtime.world_create.pregen.console": "§6Pregeneração §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Pregeneração §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Regen.", "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.may_we_suggest_name_irisworld_instead_2": "§cМожно предложить название \"IrisWorldВместо этого?",
"iris.bukkit.commandiris.that_folder_already_exists": "§cЭта папка уже существует!", "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.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.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.exception_raised_during_creation_see_console_more_details": "§cИсключение, возникшее при сотворении. Смотрите консоль для более подробной информации.",
"iris.bukkit.commandiris.successfully_created_your_world": "§aУспешно создал свой мир!", "iris.bukkit.commandiris.successfully_created_your_world": "§aУспешно создал свой мир!",
@@ -1132,7 +1134,9 @@
"iris.bukkit.runtime.irisconverter.failed_convert": "§cНе удалось конвертировать: {name}", "iris.bukkit.runtime.irisconverter.failed_convert": "§cНе удалось конвертировать: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Преобразовано: {get} в {value}", "iris.bukkit.runtime.irisconverter.converted_3": "§7Преобразовано: {get} в {value}",
"iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§cНекоторые схемы не удалось конвертировать. Проверьте консоль для деталей.", "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.looking_package": "В поисках пакета: {type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "Найден {type}.iris в {WORKSPACENAME} папка", "iris.bukkit.runtime.studiosvc.found_iris_folder": "Найден {type}.iris в {WORKSPACENAME} папка",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "Найден {type} измерение {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.finalize_open": "Завершение",
"iris.runtime.studio.stage.cleanup": "Очистка", "iris.runtime.studio.stage.cleanup": "Очистка",
"iris.runtime.world_create.teleport_failed": "§eМир был создан, но автоматический телепорт провалился. Попробуйте /iris teleport world={world}", "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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6генерировать §e{percent}%§7 {generated}/{required} чанки§8 ({remaining} осталось)", "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.action": "{bar}§7 §e{percent}%§7 | §fгенерирующий",
"iris.runtime.world_create.pregen.console": "§6генерирующий §e{percent}%", "iris.runtime.world_create.pregen.console": "§6генерирующий §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Реген", "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.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.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.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.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.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ı!", "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.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.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.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.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_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", "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.finalize_open": "Tamamlanıyor",
"iris.runtime.studio.stage.cleanup": "Temizleniyor", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6Genating §e{percent}%§7 {generated}/{required} chunk§8 ({remaining} sol)", "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.action": "{bar}§7 §e{percent}%§7 | §fPregeneating",
"iris.runtime.world_create.pregen.console": "§6Pregeneating §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Pregeneating §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Rejen", "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.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.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.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.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.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!", "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.failed_convert": "§cLỗi chuyển đổi: {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7Chuyển đổi: {get} trong {value}", "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.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.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_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", "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.finalize_open": "Đang hoàn tất",
"iris.runtime.studio.stage.cleanup": "Đang dọn dẹp", "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.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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"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.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.action": "{bar}§7 §e{percent}%§7 | §fTạo ra",
"iris.runtime.world_create.pregen.console": "§6Tạo ra §e{percent}%", "iris.runtime.world_create.pregen.console": "§6Tạo ra §e{percent}%",
"iris.runtime.chunk_job.title.regen": "Bản sao", "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.may_we_suggest_name_irisworld_instead_2": "§c建议改用名称 \"IrisWorld\"。",
"iris.bukkit.commandiris.that_folder_already_exists": "§c该文件夹已存在!", "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.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.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.exception_raised_during_creation_see_console_more_details": "§c创建期间发生异常。详情请查看控制台。",
"iris.bukkit.commandiris.successfully_created_your_world": "§a世界创建成功!", "iris.bukkit.commandiris.successfully_created_your_world": "§a世界创建成功!",
@@ -1132,7 +1134,9 @@
"iris.bukkit.runtime.irisconverter.failed_convert": "§c转换失败 : {name}", "iris.bukkit.runtime.irisconverter.failed_convert": "§c转换失败 : {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7已转换 : {get} 输入 {value}", "iris.bukkit.runtime.irisconverter.converted_3": "§7已转换 : {get} 输入 {value}",
"iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c一些示意图未能转换. 检查控制台的细节。", "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.looking_package": "正在查找内容包:{type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "在 {WORKSPACENAME} 文件夹中找到 {type}.iris", "iris.bukkit.runtime.studiosvc.found_iris_folder": "在 {WORKSPACENAME} 文件夹中找到 {type}.iris",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "在 {WORKSPACENAME} 文件夹中找到 {type} 维度,正在重新打包", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "在 {WORKSPACENAME} 文件夹中找到 {type} 维度,正在重新打包",
@@ -1203,8 +1207,28 @@
"iris.runtime.studio.stage.finalize_open": "最后确定", "iris.runtime.studio.stage.finalize_open": "最后确定",
"iris.runtime.studio.stage.cleanup": "清理", "iris.runtime.studio.stage.cleanup": "清理",
"iris.runtime.world_create.teleport_failed": "§e世界是创建的,但自动电传传送失败. 尝试 /iris teleport world={world}", "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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6正在生成 §e{percent}%§7 {generated}/{required} 块§8 ({remaining} 左侧)", "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.action": "{bar}§7 §e{percent}%§7 | §f预生成",
"iris.runtime.world_create.pregen.console": "§6预生成 §e{percent}%", "iris.runtime.world_create.pregen.console": "§6预生成 §e{percent}%",
"iris.runtime.chunk_job.title.regen": "瑞根,你好吗?", "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.may_we_suggest_name_irisworld_instead_2": "§c建議改用名稱 \"IrisWorld\"。",
"iris.bukkit.commandiris.that_folder_already_exists": "§c該資料夾已存在!", "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.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.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.exception_raised_during_creation_see_console_more_details": "§c建立期間發生例外。詳細資訊請查看主控台。",
"iris.bukkit.commandiris.successfully_created_your_world": "§a世界建立成功!", "iris.bukkit.commandiris.successfully_created_your_world": "§a世界建立成功!",
@@ -1132,7 +1134,9 @@
"iris.bukkit.runtime.irisconverter.failed_convert": "§c轉換失敗 : {name}", "iris.bukkit.runtime.irisconverter.failed_convert": "§c轉換失敗 : {name}",
"iris.bukkit.runtime.irisconverter.converted_3": "§7已轉換 : {get} 輸入 {value}", "iris.bukkit.runtime.irisconverter.converted_3": "§7已轉換 : {get} 輸入 {value}",
"iris.bukkit.runtime.irisconverter.some_schematics_failed_convert_check_console_details": "§c一些示意圖未能轉換. 檢查控制檯的細節。", "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.looking_package": "正在尋找內容包:{type}",
"iris.bukkit.runtime.studiosvc.found_iris_folder": "在 {WORKSPACENAME} 資料夾中找到 {type}.iris", "iris.bukkit.runtime.studiosvc.found_iris_folder": "在 {WORKSPACENAME} 資料夾中找到 {type}.iris",
"iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "在 {WORKSPACENAME} 資料夾中找到 {type} 維度,正在重新打包", "iris.bukkit.runtime.studiosvc.found_dimension_folder_repackaging": "在 {WORKSPACENAME} 資料夾中找到 {type} 維度,正在重新打包",
@@ -1203,8 +1207,28 @@
"iris.runtime.studio.stage.finalize_open": "最後確定", "iris.runtime.studio.stage.finalize_open": "最後確定",
"iris.runtime.studio.stage.cleanup": "清理", "iris.runtime.studio.stage.cleanup": "清理",
"iris.runtime.world_create.teleport_failed": "§e世界是建立的,但自動電傳傳送失敗. 嘗試 /iris teleport world={world}", "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.bossbar.working": "§6World §b{world}§7 | §fStarting",
"iris.runtime.world_create.console": "§6正在生成 §e{percent}%§7 {generated}/{required} 塊§8 ({remaining} 左側)", "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.action": "{bar}§7 §e{percent}%§7 | §f預生成",
"iris.runtime.world_create.pregen.console": "§6預生成 §e{percent}%", "iris.runtime.world_create.pregen.console": "§6預生成 §e{percent}%",
"iris.runtime.chunk_job.title.regen": "瑞根,你好嗎?", "iris.runtime.chunk_job.title.regen": "瑞根,你好嗎?",
@@ -16,9 +16,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -188,6 +190,68 @@ public class IrisDatapackCompilerTest {
assertFalse(Files.exists(datapackRoot.resolve("data/minecraft/dimension/the_end.json"))); 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 @Test
public void rejectsBindingToMissingDimension() throws Exception { public void rejectsBindingToMissingDimension() throws Exception {
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath(); 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.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.FileTime; import java.nio.file.attribute.FileTime;
import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -428,6 +429,26 @@ public class ServerConfiguratorDatapackFingerprintTest {
assertFalse(ServerConfigurator.reusableRuntimeFingerprint(null, "abc")); 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 @Test
public void restoredCompilerInputFingerprintRequiresReadyNonRestartingRuntime() throws Exception { public void restoredCompilerInputFingerprintRequiresReadyNonRestartingRuntime() throws Exception {
Field ready = ServerConfigurator.class.getDeclaredField("loadedDatapackRuntimeReady"); Field ready = ServerConfigurator.class.getDeclaredField("loadedDatapackRuntimeReady");
@@ -30,11 +30,9 @@ public class PackBiomeLayerValidatorTest {
} }
@Test @Test
public void respectsSingleEntryDefaultsWhenFieldsAreAbsent() throws Exception { public void respectsIndependentDefaultsWhenFieldsAreAbsent() throws Exception {
File pack = temporaryFolder.newFolder("pack"); File pack = temporaryFolder.newFolder("pack");
// Both fields default to one entry, so omitting both is legal...
write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}"); 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\":[{},{}]}"); write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}");
assertEquals(List.of( 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.")); "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 { private void write(File root, String relative, String content) throws Exception {
Path target = new File(root, relative).toPath(); Path target = new File(root, relative).toPath();
Files.createDirectories(target.getParent()); Files.createDirectories(target.getParent());
@@ -132,6 +132,31 @@ public class StudioOpenCoordinatorOpenKindTest {
assertTrue(method.contains("abandonment.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS")); 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 @Test
public void openFinalizerReturnsToTheServerThreadBeforeCompletion() throws Exception { public void openFinalizerReturnsToTheServerThreadBeforeCompletion() throws Exception {
String source = Files.readString(Path.of( String source = Files.readString(Path.of(
@@ -183,20 +183,20 @@ public class PackDownloadProgressReporterTest {
} }
@Test @Test
public void playerHudUsesArbitratedActionAndBossBarLanesWithCleanup() throws Exception { public void playerHudAlwaysShowsLoaderLaneAndActionBarWithCleanup() throws Exception {
String source = Files.readString(Path.of( String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java" "src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java"
)); ));
assertTrue(source.contains("new HudSlotRequest(")); assertFalse(source.contains("HudSlotRequest"));
assertTrue(source.contains("List.of(HudSurface.ACTION_BAR, HudSurface.BOSS_BAR)")); assertFalse(source.contains("HudSurface"));
assertTrue(source.contains("J.ar(this::pulseHud, HUD_PULSE_TICKS)")); 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("HUD_TERMINAL_TICKS, retiredCleanup"));
assertTrue(source.contains("BukkitPlatform.hudLanes().retire(playerId, hudLaneId)")); 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)")); assertFalse(source.contains("J.runGlobal(cleanup)"));
assertTrue(source.contains("claim.release();"));
assertTrue(source.contains("J.car(activeTaskId);")); 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 air = mock(PlatformBlockState.class);
PlatformBlockState decorant = mock(PlatformBlockState.class); PlatformBlockState decorant = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true); 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.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(1);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); 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.isAir()).thenReturn(false);
when(occupied.isFluid()).thenReturn(false); when(occupied.isFluid()).thenReturn(false);
when(decorant.key()).thenReturn("minecraft:tall_grass"); 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.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(3);
when(decorator.getTopThreshold()).thenReturn(0.75); when(decorator.getTopThreshold()).thenReturn(0.75);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
@@ -280,6 +283,7 @@ public class DecoratorCoreTest {
IrisDecorator decorator = mock(IrisDecorator.class); IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class);
PlatformBlockState decorant = mock(PlatformBlockState.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.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(3);
when(decorator.getTopThreshold()).thenReturn(1.0); when(decorator.getTopThreshold()).thenReturn(1.0);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); 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 lower = mock(PlatformBlockState.class);
PlatformBlockState upper = mock(PlatformBlockState.class); PlatformBlockState upper = mock(PlatformBlockState.class);
PlatformBlockState plant = tallPlantState(lower, upper); PlatformBlockState plant = tallPlantState(lower, upper);
when(plant.canPlaceOnto(support)).thenReturn(true);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant); when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1); Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
@@ -359,6 +364,26 @@ public class DecoratorCoreTest {
assertSame(upper, output.get(0, 2, 0)); 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 @Test
public void forcedSurfaceBlockStillPlacesDecorantAboveIt() { public void forcedSurfaceBlockStillPlacesDecorantAboveIt() {
IrisDecorator decorator = mock(IrisDecorator.class); IrisDecorator decorator = mock(IrisDecorator.class);
@@ -384,6 +409,26 @@ public class DecoratorCoreTest {
assertSame(wheat, output.get(0, 1, 0)); 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() { private PlatformBlockState airState() {
PlatformBlockState air = mock(PlatformBlockState.class); PlatformBlockState air = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true); when(air.isAir()).thenReturn(true);
@@ -65,6 +65,7 @@ public class IrisShoreLineDecoratorTest {
Fixture fixture = createFixture(false); Fixture fixture = createFixture(false);
PlatformBlockState support = sturdyState(); PlatformBlockState support = sturdyState();
PlatformBlockState targetAir = airState(); PlatformBlockState targetAir = airState();
when(fixture.decorant.canPlaceOnto(support)).thenReturn(true);
Hunk<PlatformBlockState> output = output(support, targetAir); Hunk<PlatformBlockState> output = output(support, targetAir);
fixture.shoreline.decorate(0, 0, 0, 1, -1, 0, 1, -1, fixture.shoreline.decorate(0, 0, 0, 1, -1, 0, 1, -1,
@@ -274,6 +274,27 @@ public class NativeStructureOwnershipStoreTest {
assertEquals(2, storage.flushes); 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 @Test
public void cleanFlushPerformsNoStorageIo() { public void cleanFlushPerformsNoStorageIo() {
Engine engine = engine(); Engine engine = engine();
@@ -356,6 +377,29 @@ public class NativeStructureOwnershipStoreTest {
assertEquals(2, storage.flushes); 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() { private static Engine engine() {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
when(engine.isClosing()).thenReturn(false); when(engine.isClosing()).thenReturn(false);
@@ -434,8 +478,9 @@ public class NativeStructureOwnershipStoreTest {
} }
@Override @Override
public void flush() { public boolean flush() {
flushes++; flushes++;
return true;
} }
NativeStructureOwnershipRecord find(long target, NativeStructureOwnershipRecord find(long target,
@@ -450,10 +495,11 @@ public class NativeStructureOwnershipStoreTest {
protected final Map<Long, NativeStructureOwnershipBundle> durable = new ConcurrentHashMap<>(); protected final Map<Long, NativeStructureOwnershipBundle> durable = new ConcurrentHashMap<>();
@Override @Override
public void flush() { public boolean flush() {
super.flush(); super.flush();
durable.clear(); durable.clear();
durable.putAll(chunks); durable.putAll(chunks);
return true;
} }
void crash() { void crash() {
@@ -466,13 +512,29 @@ public class NativeStructureOwnershipStoreTest {
private final AtomicBoolean fail = new AtomicBoolean(true); private final AtomicBoolean fail = new AtomicBoolean(true);
@Override @Override
public void flush() { public boolean flush() {
flushes++; flushes++;
if (fail.compareAndSet(true, false)) { if (fail.compareAndSet(true, false)) {
throw new IllegalStateException("Simulated ownership flush failure"); throw new IllegalStateException("Simulated ownership flush failure");
} }
durable.clear(); durable.clear();
durable.putAll(chunks); 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<>(); Map<String, IrisBiome> customBiomes = new HashMap<>();
customBiomes.put("shared", biome); 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( Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod(
"paintBoundaryZone", "paintBoundaryZone",
Hunk.class, Hunk.class,
MatterCavern.class, CarveWallBuffer.class,
int.class, int.class,
int.class, int.class,
int.class, int.class,
@@ -86,7 +88,7 @@ public class IrisCarveModifierInferenceIsolationTest {
paintBoundaryZone.invoke( paintBoundaryZone.invoke(
modifier, modifier,
mock(Hunk.class), mock(Hunk.class),
new MatterCavern(true, "shared", (byte) 0), walls,
0, 0,
0, 0,
0, 0,
@@ -30,6 +30,9 @@ public class IrisCarveScratchTest {
buffer.put(5, 25, 15, replacement); buffer.put(5, 25, 15, replacement);
expected.put(key(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<>(); Map<String, MatterCavern> actual = new HashMap<>();
buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern)); buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern));
assertEquals(expected.keySet(), actual.keySet()); assertEquals(expected.keySet(), actual.keySet());
@@ -44,7 +47,6 @@ public class IrisCarveScratchTest {
MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0); MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0);
scratch.columnMasks[0].add(12); scratch.columnMasks[0].add(12);
scratch.boundaryMasks[0].add(13); scratch.boundaryMasks[0].add(13);
scratch.boundaryCaverns[0] = cavern;
scratch.walls.put(1, 12, 2, cavern); scratch.walls.put(1, 12, 2, cavern);
scratch.customBiomeCache.put("cave", null); scratch.customBiomeCache.put("cave", null);
scratch.customCaveBiomePresent = true; scratch.customCaveBiomePresent = true;
@@ -53,7 +55,6 @@ public class IrisCarveScratchTest {
assertTrue(scratch.columnMasks[0].isEmpty()); assertTrue(scratch.columnMasks[0].isEmpty());
assertTrue(scratch.boundaryMasks[0].isEmpty()); assertTrue(scratch.boundaryMasks[0].isEmpty());
assertNull(scratch.boundaryCaverns[0]);
assertTrue(scratch.customBiomeCache.isEmpty()); assertTrue(scratch.customBiomeCache.isEmpty());
assertFalse(scratch.customCaveBiomePresent); assertFalse(scratch.customCaveBiomePresent);
int[] wallCount = new int[1]; int[] wallCount = new int[1];
@@ -18,7 +18,11 @@
package art.arcane.iris.engine.modifier; 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.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -32,6 +36,23 @@ public class IrisDepositModifierContainmentTest {
public void depositSurfaceLimitKeepsEveryColumnSevenBlocksBuried() { public void depositSurfaceLimitKeepsEveryColumnSevenBlocksBuried() {
assertEquals(73, IrisDepositModifier.depositSurfaceLimit(80)); assertEquals(73, IrisDepositModifier.depositSurfaceLimit(80));
assertEquals(24, IrisDepositModifier.depositSurfaceLimit(31)); 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 @Test
@@ -70,4 +91,85 @@ public class IrisDepositModifierContainmentTest {
assertEquals(2, IrisDepositModifier.clampDepositCenter(1, 4, 16)); assertEquals(2, IrisDepositModifier.clampDepositCenter(1, 4, 16));
assertEquals(14, IrisDepositModifier.clampDepositCenter(15, 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()); 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.core.loader.IrisData;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -52,6 +53,69 @@ public class IrisDepositTuningTest {
assertFalse(stoneGenerator.isOre(data)); 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) { private IrisDepositGenerator generatorWithState(IrisData data, boolean ore) {
IrisBlockData block = mock(IrisBlockData.class); IrisBlockData block = mock(IrisBlockData.class);
PlatformBlockState state = mock(PlatformBlockState.class); PlatformBlockState state = mock(PlatformBlockState.class);
@@ -90,6 +90,17 @@ public class NativeStructureObjectVetoTest {
assertTrue(placer.written().isEmpty()); 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 @Test
public void pieceOverlappingOnlyTheEnvelopeStillPlaces() { public void pieceOverlappingOnlyTheEnvelopeStillPlaces() {
RecordingPlacer placer = new RecordingPlacer(engine); 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);
}
}