mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Correct Java 26.2 Iris world lifecycle
This commit is contained in:
+115
@@ -14,6 +14,7 @@ import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
@@ -53,6 +54,8 @@ import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import io.papermc.paper.world.saveddata.PaperLevelOverrides;
|
||||
import io.papermc.paper.world.saveddata.PaperWorldMetadata;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
@@ -116,6 +119,8 @@ import net.minecraft.world.level.levelgen.structure.StructureCheck;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||
import net.minecraft.world.level.storage.PrimaryLevelData;
|
||||
import net.minecraft.world.level.storage.SavedDataStorage;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
||||
@@ -151,10 +156,14 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
@@ -163,11 +172,18 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class NMSBinding implements INMSBinding {
|
||||
private static final long CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS = 30L;
|
||||
|
||||
private final KMap<Biome, Object> baseBiomeCache = new KMap<>();
|
||||
private volatile DataVersion dataVersion;
|
||||
private final BlockData AIR = Material.AIR.createBlockData();
|
||||
@@ -1593,6 +1609,105 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCurrentPaperWorldData(
|
||||
Path sourceWorldDirectory,
|
||||
Path targetWorldDirectory,
|
||||
long seed
|
||||
) throws IOException {
|
||||
CraftServer craftServer = (CraftServer) Bukkit.getServer();
|
||||
MinecraftServer server = craftServer.getHandle().getServer();
|
||||
PaperLevelOverrides levelOverrides = captureCurrentPaperLevelOverrides(craftServer, server);
|
||||
|
||||
Path targetWorld = targetWorldDirectory.toAbsolutePath().normalize();
|
||||
UUID metadataUuid = UUID.randomUUID();
|
||||
WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorldDirectory, targetWorld, seed);
|
||||
try (SavedDataStorage savedDataStorage = new SavedDataStorage(
|
||||
targetWorld.resolve("data"),
|
||||
server.getFixerUpper(),
|
||||
server.registryAccess()
|
||||
)) {
|
||||
savedDataStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(metadataUuid));
|
||||
savedDataStorage.set(
|
||||
PaperLevelOverrides.TYPE,
|
||||
levelOverrides
|
||||
);
|
||||
}
|
||||
|
||||
List<Path> requiredDataFiles = List.of(
|
||||
targetWorld.resolve("data/minecraft/world_gen_settings.dat"),
|
||||
targetWorld.resolve("data/paper/metadata.dat"),
|
||||
targetWorld.resolve("data/paper/level_overrides.dat")
|
||||
);
|
||||
for (Path requiredDataFile : requiredDataFiles) {
|
||||
if (!Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Current Paper world data was not written: " + requiredDataFile);
|
||||
}
|
||||
}
|
||||
|
||||
long writtenSeed = WorldReplacementSeed.readAuthoritativeSeed(targetWorld);
|
||||
if (writtenSeed != seed) {
|
||||
throw new IOException("Current Paper world data did not retain the requested seed.");
|
||||
}
|
||||
|
||||
try (SavedDataStorage verificationStorage = new SavedDataStorage(
|
||||
targetWorld.resolve("data"),
|
||||
server.getFixerUpper(),
|
||||
server.registryAccess()
|
||||
)) {
|
||||
PaperWorldMetadata metadata = verificationStorage.get(PaperWorldMetadata.TYPE);
|
||||
if (metadata == null || !metadataUuid.equals(metadata.uuid())) {
|
||||
throw new IOException("Current Paper world metadata could not be verified.");
|
||||
}
|
||||
PaperLevelOverrides overrides = verificationStorage.get(PaperLevelOverrides.TYPE);
|
||||
if (overrides == null || overrides.isInitialized()) {
|
||||
throw new IOException("Current Paper level overrides could not be verified.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PaperLevelOverrides captureCurrentPaperLevelOverrides(
|
||||
CraftServer craftServer,
|
||||
MinecraftServer server
|
||||
) throws IOException {
|
||||
if (craftServer.isGlobalTickThread()) {
|
||||
return createCurrentPaperLevelOverrides(server);
|
||||
}
|
||||
if (J.isFolia() && J.isPrimaryThread()) {
|
||||
throw new IOException("Current Paper world data cannot be staged from a Folia region tick thread.");
|
||||
}
|
||||
|
||||
CompletableFuture<PaperLevelOverrides> captured = new CompletableFuture<>();
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
try {
|
||||
captured.complete(createCurrentPaperLevelOverrides(server));
|
||||
} catch (Throwable failure) {
|
||||
captured.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
throw new IOException("Could not schedule the current Paper level-data snapshot on the global thread.");
|
||||
}
|
||||
|
||||
try {
|
||||
return captured.get(CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while capturing current Paper level data.", failure);
|
||||
} catch (ExecutionException failure) {
|
||||
throw new IOException("Could not capture current Paper level data.", failure.getCause());
|
||||
} catch (TimeoutException failure) {
|
||||
throw new IOException("Timed out while capturing current Paper level data.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private PaperLevelOverrides createCurrentPaperLevelOverrides(MinecraftServer server) throws IOException {
|
||||
if (!(server.getWorldData().overworldData() instanceof PrimaryLevelData primaryLevelData)) {
|
||||
throw new IOException("Paper primary level data is unavailable for current world data staging.");
|
||||
}
|
||||
return PaperLevelOverrides.createFromLiveLevelData(primaryLevelData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingCurrentPaperWorldDataContractTest {
|
||||
@Test
|
||||
public void stagesAllCurrentPaperWorldDataFromLiveServerState() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||
String writer = section(
|
||||
source,
|
||||
"public void writeCurrentPaperWorldData(",
|
||||
"public KMap<Material, List<BlockProperty>> getBlockProperties()"
|
||||
);
|
||||
|
||||
assertTrue(writer.contains("WorldReplacementSeed.copyWithAuthoritativeSeed("));
|
||||
assertTrue(writer.contains("UUID metadataUuid = UUID.randomUUID()"));
|
||||
assertTrue(writer.contains("new PaperWorldMetadata(metadataUuid)"));
|
||||
assertTrue(writer.contains("captureCurrentPaperLevelOverrides(craftServer, server)"));
|
||||
assertTrue(writer.contains("new SavedDataStorage("));
|
||||
assertTrue(writer.contains("server.getFixerUpper()"));
|
||||
assertTrue(writer.contains("server.registryAccess()"));
|
||||
assertTrue(writer.contains("data/minecraft/world_gen_settings.dat"));
|
||||
assertTrue(writer.contains("data/paper/metadata.dat"));
|
||||
assertTrue(writer.contains("data/paper/level_overrides.dat"));
|
||||
assertTrue(writer.contains("WorldReplacementSeed.readAuthoritativeSeed(targetWorld)"));
|
||||
assertTrue(writer.contains("verificationStorage.get(PaperWorldMetadata.TYPE)"));
|
||||
assertTrue(writer.contains("metadataUuid.equals(metadata.uuid())"));
|
||||
assertTrue(writer.contains("verificationStorage.get(PaperLevelOverrides.TYPE)"));
|
||||
assertTrue(writer.contains("overrides == null || overrides.isInitialized()"));
|
||||
assertTrue(writer.contains("Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)"));
|
||||
assertFalse(writer.toLowerCase().contains("migrat"));
|
||||
assertFalse(writer.toLowerCase().contains("fallback"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capturesOnlyLiveLevelOverridesOnTheGlobalThread() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||
String capture = section(
|
||||
source,
|
||||
"private PaperLevelOverrides captureCurrentPaperLevelOverrides(",
|
||||
"private PaperLevelOverrides createCurrentPaperLevelOverrides("
|
||||
);
|
||||
String create = section(
|
||||
source,
|
||||
"private PaperLevelOverrides createCurrentPaperLevelOverrides(",
|
||||
"public KMap<Material, List<BlockProperty>> getBlockProperties()"
|
||||
);
|
||||
|
||||
assertTrue(capture.contains("craftServer.isGlobalTickThread()"));
|
||||
assertTrue(capture.contains("J.isFolia() && J.isPrimaryThread()"));
|
||||
assertTrue(capture.contains("J.runGlobal("));
|
||||
assertTrue(capture.contains("captured.get(CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS"));
|
||||
assertTrue(capture.contains("Thread.currentThread().interrupt()"));
|
||||
assertTrue(create.contains("PaperLevelOverrides.createFromLiveLevelData(primaryLevelData)"));
|
||||
assertFalse(capture.contains("WorldReplacementSeed"));
|
||||
assertFalse(capture.contains("SavedDataStorage"));
|
||||
assertFalse(capture.contains("Files."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedBindingsRejectCurrentPaperWorldDataStaging() {
|
||||
INMSBinding binding = (INMSBinding) Proxy.newProxyInstance(
|
||||
INMSBinding.class.getClassLoader(),
|
||||
new Class<?>[]{INMSBinding.class},
|
||||
(proxy, method, arguments) -> InvocationHandler.invokeDefault(proxy, method, arguments)
|
||||
);
|
||||
|
||||
UnsupportedOperationException error = assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> binding.writeCurrentPaperWorldData(Path.of("source"), Path.of("target"), 1L)
|
||||
);
|
||||
assertTrue(error.getMessage().contains("does not support current Paper world data staging"));
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
int start = source.indexOf(startMarker);
|
||||
int end = source.indexOf(endMarker, start);
|
||||
assertTrue("Missing source section starting with " + startMarker, start >= 0);
|
||||
assertTrue("Missing source section ending with " + endMarker, end > start);
|
||||
return source.substring(start, end);
|
||||
}
|
||||
}
|
||||
+29
-6
@@ -89,14 +89,17 @@ public final class BukkitWorldReconciler {
|
||||
|
||||
DimensionResolution dimensionResolution;
|
||||
Long configuredSeed;
|
||||
String configuredWorldName;
|
||||
try {
|
||||
configuredWorldName = backend.configuredWorldName(worldKey);
|
||||
dimensionResolution = backend.resolveDimension(worldKey);
|
||||
configuredSeed = dimensionResolution.succeeded()
|
||||
? backend.configuredSeed(IrisWorldStorage.logicalName(worldKey))
|
||||
? backend.configuredSeed(configuredWorldName)
|
||||
: null;
|
||||
} catch (Throwable failure) {
|
||||
dimensionResolution = DimensionResolution.failed(failure);
|
||||
configuredSeed = null;
|
||||
configuredWorldName = IrisWorldStorage.logicalName(worldKey);
|
||||
}
|
||||
if (!dimensionResolution.succeeded()) {
|
||||
lease.close();
|
||||
@@ -106,6 +109,7 @@ public final class BukkitWorldReconciler {
|
||||
}
|
||||
return loadWithLease(
|
||||
configurationFile,
|
||||
configuredWorldName,
|
||||
worldKey,
|
||||
dimensionResolution.dimension(),
|
||||
configuredSeed,
|
||||
@@ -137,7 +141,7 @@ public final class BukkitWorldReconciler {
|
||||
}
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
worldKey = IrisWorldStorage.keyFromName(worldName);
|
||||
worldKey = backend.worldKeyFromConfiguration(worldName);
|
||||
} catch (Throwable failure) {
|
||||
chain = chain.thenApply(results -> {
|
||||
results.add(LoadResult.validationFailure(worldName, failure));
|
||||
@@ -158,6 +162,7 @@ public final class BukkitWorldReconciler {
|
||||
}
|
||||
chain = chain.thenCompose(results -> loadConfiguredWorld(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
worldName,
|
||||
worldKey,
|
||||
dimension,
|
||||
seed)
|
||||
@@ -176,6 +181,7 @@ public final class BukkitWorldReconciler {
|
||||
|
||||
private CompletableFuture<LoadResult> loadConfiguredWorld(
|
||||
File configurationFile,
|
||||
String configuredWorldName,
|
||||
NamespacedKey worldKey,
|
||||
String dimension,
|
||||
Long seed
|
||||
@@ -187,7 +193,7 @@ public final class BukkitWorldReconciler {
|
||||
return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure));
|
||||
}
|
||||
|
||||
return loadWithLease(configurationFile, worldKey, dimension, seed, lease);
|
||||
return loadWithLease(configurationFile, configuredWorldName, worldKey, dimension, seed, lease);
|
||||
}
|
||||
|
||||
private LifecycleOperationCoordinator.Lease acquireWorldLoad(NamespacedKey worldKey) {
|
||||
@@ -199,6 +205,7 @@ public final class BukkitWorldReconciler {
|
||||
|
||||
private CompletableFuture<LoadResult> loadWithLease(
|
||||
File configurationFile,
|
||||
String configuredWorldName,
|
||||
NamespacedKey worldKey,
|
||||
String dimension,
|
||||
Long seed,
|
||||
@@ -214,11 +221,10 @@ public final class BukkitWorldReconciler {
|
||||
}
|
||||
|
||||
BukkitWorldConfiguration.Registration registration;
|
||||
String worldName = IrisWorldStorage.logicalName(worldKey);
|
||||
try {
|
||||
registration = BukkitWorldConfiguration.register(
|
||||
configurationFile,
|
||||
worldName,
|
||||
configuredWorldName,
|
||||
dimension,
|
||||
seed);
|
||||
} catch (Throwable failure) {
|
||||
@@ -250,7 +256,7 @@ public final class BukkitWorldReconciler {
|
||||
try {
|
||||
boolean rolledBack = BukkitWorldConfiguration.removeIfMatching(
|
||||
configurationFile,
|
||||
worldName,
|
||||
configuredWorldName,
|
||||
dimension,
|
||||
seed);
|
||||
return new LoadResult(settled, registration, true, rolledBack, null);
|
||||
@@ -426,6 +432,10 @@ public final class BukkitWorldReconciler {
|
||||
|
||||
Long configuredSeed(String worldName);
|
||||
|
||||
String configuredWorldName(NamespacedKey worldKey);
|
||||
|
||||
NamespacedKey worldKeyFromConfiguration(String configuredWorldName);
|
||||
|
||||
Optional<World> loadedWorld(NamespacedKey worldKey);
|
||||
|
||||
CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed);
|
||||
@@ -667,6 +677,19 @@ public final class BukkitWorldReconciler {
|
||||
return IrisWorlds.readBukkitWorldSeed(worldName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String configuredWorldName(NamespacedKey worldKey) {
|
||||
return IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public NamespacedKey worldKeyFromConfiguration(String configuredWorldName) {
|
||||
return IrisWorldStorage.keyFromConfiguredWorldName(
|
||||
configuredWorldName,
|
||||
IrisWorldStorage.levelRoot().getName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<World> loadedWorld(NamespacedKey worldKey) {
|
||||
return WorldIdentity.resolve(worldKey);
|
||||
|
||||
+7
-2
@@ -165,7 +165,8 @@ public final class IrisWorldGeneratorResolver {
|
||||
|
||||
@Nullable
|
||||
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
|
||||
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
|
||||
NamespacedKey worldKey = configuredWorldKey(worldName, IrisWorldStorage.levelRoot().getName());
|
||||
File pack = IrisWorldStorage.packRoot(worldKey);
|
||||
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||
if (dimension == null) {
|
||||
@@ -182,6 +183,10 @@ public final class IrisWorldGeneratorResolver {
|
||||
return dimension;
|
||||
}
|
||||
|
||||
static NamespacedKey configuredWorldKey(String worldName, String levelName) {
|
||||
return IrisWorldStorage.keyFromConfiguredWorldName(worldName, levelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the biome provider for a world, falling back to the supplied Bukkit default when
|
||||
* Iris has nothing staged.
|
||||
@@ -212,7 +217,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
if (dim == null) {
|
||||
throw new RuntimeException("Can't find dimension " + id + "!");
|
||||
}
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
|
||||
NamespacedKey worldKey = configuredWorldKey(worldName, IrisWorldStorage.levelRoot().getName());
|
||||
File snapshotRoot = IrisWorldStorage.packRoot(worldKey);
|
||||
File dimensionPackRoot = dim.getLoader().getDataFolder();
|
||||
String packName = dimensionPackRoot.getName();
|
||||
|
||||
+34
-62
@@ -13,6 +13,7 @@ import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem.ReplacementPath
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
@@ -33,7 +34,6 @@ import org.bukkit.event.world.WorldLoadEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@@ -56,19 +56,10 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
|
||||
public NamespacedKey resolveRequestedWorldKey(String requestedName) {
|
||||
String requested = Objects.requireNonNull(requestedName, "requestedName").trim();
|
||||
if (requested.isEmpty()) {
|
||||
throw new IllegalArgumentException("World name cannot be empty.");
|
||||
}
|
||||
if (requested.contains("/") || requested.contains("\\") || requested.contains("..")) {
|
||||
throw new IllegalArgumentException("World name must be a safe single path segment.");
|
||||
}
|
||||
NamespacedKey worldKey = requested.contains(":")
|
||||
? NamespacedKey.fromString(requested.toLowerCase(Locale.ENGLISH))
|
||||
: IrisWorldStorage.keyFromName(requested);
|
||||
if (worldKey == null) {
|
||||
throw new IllegalArgumentException("World identifier is invalid: " + requestedName);
|
||||
}
|
||||
NamespacedKey worldKey = IrisWorldStorage.replacementKeyFromName(
|
||||
requestedName,
|
||||
IrisWorldStorage.levelRoot().getName()
|
||||
);
|
||||
ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), toWorldSlotKey(worldKey));
|
||||
return worldKey;
|
||||
}
|
||||
@@ -76,14 +67,13 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
public synchronized StagedReplacement stageReplacement(
|
||||
VolmitSender sender,
|
||||
NamespacedKey worldKey,
|
||||
IrisDimension dimension,
|
||||
long seed
|
||||
IrisDimension dimension
|
||||
) throws IOException {
|
||||
VolmitSender requiredSender = Objects.requireNonNull(sender, "sender");
|
||||
NamespacedKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey");
|
||||
WorldSlotKey requiredWorldSlotKey = toWorldSlotKey(requiredWorldKey);
|
||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||
IrisStartupValidation.requireWorldCreationReady();
|
||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||
if (!WorldReplacementBootstrapMarker.wasBootstrappedThisProcess()) {
|
||||
throw new IOException("Exact world replacement requires a full Paper-family startup bootstrap.");
|
||||
}
|
||||
@@ -97,18 +87,19 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
if (findTransaction(requiredWorldSlotKey) != null) {
|
||||
throw new IOException("A replacement is already pending for " + requiredWorldKey + ".");
|
||||
}
|
||||
ExactWorldSlotPathPolicy.Target target = prepareTarget(requiredWorldSlotKey);
|
||||
ExactWorldSlotPathPolicy.Target target = resolveTarget(requiredWorldSlotKey);
|
||||
requireCompatibleEnvironment(target.slotKind(), requiredDimension.getEnvironment());
|
||||
long effectiveSeed = resolveEffectiveSeed(target.slotKind(), seed);
|
||||
requireVanillaSlotEnabled(target.slotKind());
|
||||
UUID transactionId = UUID.randomUUID();
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transactionId);
|
||||
WorldReplacementFilesystem.requireExistingTarget(paths);
|
||||
long effectiveSeed = WorldReplacementSeed.readAuthoritativeSeed(paths.target());
|
||||
String worldName = WorldReplacementJournal.logicalWorldName(target.levelRoot(), requiredWorldSlotKey);
|
||||
DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true);
|
||||
if (!datapacks.succeeded()) {
|
||||
throw new IOException("Iris could not compile the dimension datapacks.");
|
||||
}
|
||||
|
||||
UUID transactionId = UUID.randomUUID();
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transactionId);
|
||||
WorldReplacementFilesystem.requireExistingTarget(paths);
|
||||
boolean targetPresent = true;
|
||||
WorldGeneratorSnapshot originalConfiguration = BukkitWorldConfiguration.snapshot(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
@@ -435,23 +426,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private ExactWorldSlotPathPolicy.Target prepareTarget(WorldSlotKey worldKey) throws IOException {
|
||||
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
||||
ExactWorldSlotPathPolicy.Target target = ExactWorldSlotPathPolicy.resolve(levelRoot, worldKey);
|
||||
Path dimensions = target.levelRoot().resolve("dimensions");
|
||||
createDirectoryIfMissing(dimensions);
|
||||
createDirectoryIfMissing(target.namespaceRoot());
|
||||
return ExactWorldSlotPathPolicy.resolve(levelRoot, worldKey);
|
||||
}
|
||||
|
||||
private static void createDirectoryIfMissing(Path directory) throws IOException {
|
||||
if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (Files.isSymbolicLink(directory) || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("World storage parent is unsafe: " + directory);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Files.createDirectory(directory);
|
||||
private static ExactWorldSlotPathPolicy.Target resolveTarget(WorldSlotKey worldKey) {
|
||||
return ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), worldKey);
|
||||
}
|
||||
|
||||
private Transaction findTransaction(NamespacedKey worldKey) throws IOException {
|
||||
@@ -510,7 +486,7 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireCompatibleEnvironment(SlotKind slotKind, IrisEnvironment environment) {
|
||||
static void requireCompatibleEnvironment(SlotKind slotKind, IrisEnvironment environment) {
|
||||
IrisEnvironment expected = switch (slotKind) {
|
||||
case VANILLA_OVERWORLD -> IrisEnvironment.NORMAL;
|
||||
case VANILLA_NETHER -> IrisEnvironment.NETHER;
|
||||
@@ -524,62 +500,58 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the primary level context on the main thread at startup. resolveEffectiveSeed
|
||||
* reads this snapshot so staging never blocks on a main-thread hop while holding the
|
||||
* manager monitor — that inversion froze the server for the full 30s hop timeout whenever
|
||||
* another world loaded during staging. The context is stable for the process lifetime:
|
||||
* slot replacements only commit across a restart.
|
||||
* Captures vanilla-slot availability on the main thread at startup so staging never blocks
|
||||
* on a main-thread hop while holding the manager monitor. The context is stable for the
|
||||
* process lifetime because slot replacements only commit across a restart.
|
||||
*/
|
||||
public void captureVanillaLevelContext() {
|
||||
try {
|
||||
vanillaLevelContext = new VanillaLevelContext(
|
||||
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
|
||||
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
|
||||
.getSeed(),
|
||||
Iris.instance.getServer().getAllowNether(),
|
||||
Iris.instance.getServer().getAllowEnd()
|
||||
);
|
||||
} catch (Throwable failure) {
|
||||
Iris.debug("Could not capture the primary level context yet: " + detail(failure));
|
||||
Iris.debug("Could not capture vanilla-slot availability yet: " + detail(failure));
|
||||
}
|
||||
}
|
||||
|
||||
private static long resolveEffectiveSeed(SlotKind slotKind, long requestedSeed) throws IOException {
|
||||
if (slotKind == SlotKind.IRIS_MANAGED) {
|
||||
return requestedSeed;
|
||||
private static void requireVanillaSlotEnabled(SlotKind slotKind) throws IOException {
|
||||
if (slotKind != SlotKind.VANILLA_NETHER && slotKind != SlotKind.VANILLA_END) {
|
||||
return;
|
||||
}
|
||||
VanillaLevelContext context = vanillaLevelContext;
|
||||
if (context == null) {
|
||||
context = resolveVanillaLevelContext();
|
||||
vanillaLevelContext = context;
|
||||
}
|
||||
if (slotKind == SlotKind.VANILLA_NETHER && !context.allowNether()) {
|
||||
requireVanillaSlotEnabled(slotKind, context.allowNether(), context.allowEnd());
|
||||
}
|
||||
|
||||
static void requireVanillaSlotEnabled(SlotKind slotKind, boolean allowNether, boolean allowEnd)
|
||||
throws IOException {
|
||||
if (slotKind == SlotKind.VANILLA_NETHER && !allowNether) {
|
||||
throw new IOException("allow-nether must be true before the vanilla Nether can be replaced.");
|
||||
}
|
||||
if (slotKind == SlotKind.VANILLA_END && !context.allowEnd()) {
|
||||
if (slotKind == SlotKind.VANILLA_END && !allowEnd) {
|
||||
throw new IOException("Bukkit allow-end must be true before the vanilla End can be replaced.");
|
||||
}
|
||||
return context.seed();
|
||||
}
|
||||
|
||||
private static VanillaLevelContext resolveVanillaLevelContext() throws IOException {
|
||||
CompletableFuture<VanillaLevelContext> contextFuture = J.sfut(() -> new VanillaLevelContext(
|
||||
WorldIdentity.resolve(NamespacedKey.minecraft("overworld"))
|
||||
.orElseThrow(() -> new IllegalStateException("The configured primary world is not loaded."))
|
||||
.getSeed(),
|
||||
Iris.instance.getServer().getAllowNether(),
|
||||
Iris.instance.getServer().getAllowEnd()
|
||||
));
|
||||
if (contextFuture == null) {
|
||||
throw new IOException("Could not schedule primary level-seed resolution.");
|
||||
throw new IOException("Could not schedule vanilla-slot availability resolution.");
|
||||
}
|
||||
try {
|
||||
return contextFuture.get(30L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Primary level-seed resolution was interrupted.", failure);
|
||||
throw new IOException("Vanilla-slot availability resolution was interrupted.", failure);
|
||||
} catch (ExecutionException | TimeoutException failure) {
|
||||
throw new IOException("Could not resolve the authoritative primary level seed.", failure);
|
||||
throw new IOException("Could not resolve vanilla-slot availability.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +600,7 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
|
||||
private static volatile VanillaLevelContext vanillaLevelContext;
|
||||
|
||||
private record VanillaLevelContext(long seed, boolean allowNether, boolean allowEnd) {
|
||||
private record VanillaLevelContext(boolean allowNether, boolean allowEnd) {
|
||||
}
|
||||
|
||||
private static final class RestartBoundaryRequired extends IOException {
|
||||
|
||||
+99
-300
@@ -33,6 +33,7 @@ import art.arcane.iris.core.lifecycle.IrisWorldRemovalService;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
@@ -52,7 +53,6 @@ import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -62,28 +62,17 @@ import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
||||
import static org.bukkit.Bukkit.getServer;
|
||||
@@ -109,7 +98,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
private CommandPack pack;
|
||||
private CommandFind find;
|
||||
private CommandDatapack datapack;
|
||||
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
|
||||
VolmitSender sender = Iris.getSender();
|
||||
|
||||
@Director(description = "Create a new world", descriptionKey = "iris.director.commandiris.director.create_new_world", aliases = {"c"})
|
||||
@@ -124,29 +112,17 @@ public class CommandIris implements DirectorExecutor {
|
||||
)
|
||||
String type,
|
||||
@Param(description = "The seed to generate the world with", descriptionKey = "iris.director.commandiris.param.seed_generate_world_with", defaultValue = "1337")
|
||||
long seed,
|
||||
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", descriptionKey = "iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world", defaultValue = "false")
|
||||
boolean main,
|
||||
@Param(name = "overwrite", aliases = "force", description = "Replace the exact existing world slot on the next restart", descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart", defaultValue = "false")
|
||||
boolean overwrite
|
||||
long seed
|
||||
) {
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
if (overwrite) {
|
||||
worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(name);
|
||||
} else {
|
||||
worldKey = IrisWorldStorage.managedKeyFromName(name);
|
||||
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
sender().sendMessage(C.RED + e.getMessage());
|
||||
return;
|
||||
}
|
||||
String worldName = IrisWorldStorage.logicalName(worldKey);
|
||||
if (overwrite && main && !NamespacedKey.minecraft("overworld").equals(worldKey)) {
|
||||
sender().sendMessage(C.RED + "overwrite=true with main=true must target the configured main-world name.");
|
||||
return;
|
||||
}
|
||||
if (worldName.equalsIgnoreCase("iris")) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD));
|
||||
@@ -159,7 +135,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!overwrite && IrisWorldStorage.dimensionRoot(worldName).exists()) {
|
||||
if (IrisWorldStorage.dimensionRoot(worldName).exists()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS));
|
||||
return;
|
||||
}
|
||||
@@ -177,31 +153,13 @@ public class CommandIris implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (overwrite) {
|
||||
try {
|
||||
PendingWorldReplacementManager.StagedReplacement staged = Iris.instance
|
||||
.pendingWorldReplacements()
|
||||
.stageReplacement(sender(), worldKey, dimension, seed);
|
||||
if (staged.seed() != seed) {
|
||||
sender().sendMessage(C.YELLOW + "Exact vanilla slots preserve the shared level seed; using "
|
||||
+ staged.seed() + " instead of " + seed + ".");
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||
+ ". Restart once to publish it. The current dimension is retained until Iris verifies the replacement.");
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||
? failure.getClass().getSimpleName()
|
||||
: failure.getMessage();
|
||||
sender().sendMessage(C.RED + "Could not stage the world replacement: " + detail);
|
||||
}
|
||||
if (J.isFolia()) {
|
||||
boolean staged = stageFoliaWorldCreation(worldName, dimension, seed);
|
||||
if (!staged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
if (stageFoliaWorldCreation(worldName, dimension, seed, main)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTART_SERVER_GENERATE_LOAD, MessageArgument.untrusted("worldName", worldName)));
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD, MessageArgument.untrusted("worldName", worldName)));
|
||||
ServerConfigurator.restart("Iris staged Folia world \"" + worldName + "\" for startup.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,12 +171,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
.sender(sender())
|
||||
.studio(false)
|
||||
.create();
|
||||
if (main) {
|
||||
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
|
||||
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
|
||||
return new Thread(() -> updateMainWorld(worldName));
|
||||
}));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (reportExpectedCreationInterruption(e)) {
|
||||
return;
|
||||
@@ -229,207 +181,65 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD));
|
||||
if (main) sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOUR_WORLD_WILL_AUTOMATICALLY_BE_SET_AS_MAIN_WORLD_WHEN));
|
||||
}
|
||||
|
||||
private boolean updateMainWorld(String newName) {
|
||||
LifecycleOperationCoordinator.Lease lease;
|
||||
@Director(
|
||||
description = "Replace an existing world with Iris generation on the next restart",
|
||||
descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
|
||||
aliases = {"override", "overwrite"}
|
||||
)
|
||||
public void replace(
|
||||
@Param(
|
||||
name = "target",
|
||||
aliases = "world-name",
|
||||
description = "The exact existing world slot to replace",
|
||||
descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart"
|
||||
)
|
||||
String target,
|
||||
@Param(
|
||||
aliases = {"dimension", "pack"},
|
||||
description = "The dimension/pack to replace the world with",
|
||||
defaultValue = "default",
|
||||
customHandler = PackDimensionTypeHandler.class
|
||||
)
|
||||
String type
|
||||
) {
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
lease = LifecycleOperationCoordinator.get().acquire(
|
||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.WORLD_PROMOTE,
|
||||
newName
|
||||
);
|
||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||
Iris.error("Could not promote Iris world \"" + newName + "\": " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return updateMainWorldUnderLease(newName);
|
||||
} finally {
|
||||
lease.close();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updateMainWorldUnderLease(String newName) {
|
||||
try {
|
||||
File oldLevelRoot = IrisWorldStorage.levelRoot();
|
||||
File worldContainer = oldLevelRoot.getParentFile();
|
||||
if (worldContainer == null) {
|
||||
throw new IllegalStateException("Current level folder has no world container.");
|
||||
}
|
||||
Properties data = new Properties();
|
||||
try (FileInputStream in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.load(in);
|
||||
}
|
||||
|
||||
File sourceDimensionRoot = IrisWorldStorage.dimensionRoot(IrisWorldStorage.keyFromName(newName));
|
||||
if (!sourceDimensionRoot.isDirectory()) {
|
||||
throw new IllegalStateException("Source dimension folder does not exist: " + sourceDimensionRoot.getAbsolutePath());
|
||||
}
|
||||
|
||||
File newLevelRoot = new File(worldContainer, newName);
|
||||
World sourceWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromName(newName)).orElse(null);
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(newName);
|
||||
if (sourceWorld == null && stagedSeed == null) {
|
||||
throw new IllegalStateException("Cannot determine the promoted world's seed.");
|
||||
}
|
||||
long promotedSeed = sourceWorld == null ? stagedSeed : sourceWorld.getSeed();
|
||||
data.setProperty("level-name", newName);
|
||||
data.setProperty("level-seed", Long.toString(promotedSeed));
|
||||
|
||||
try (MainWorldPublication publication = publishMainWorldFiles(
|
||||
oldLevelRoot.toPath(),
|
||||
sourceDimensionRoot.toPath(),
|
||||
newLevelRoot.toPath()
|
||||
)) {
|
||||
writeServerPropertiesAtomically(ServerProperties.SERVER_PROPERTIES.toPath(), data);
|
||||
publication.commit();
|
||||
}
|
||||
synchronized (ServerProperties.DATA) {
|
||||
ServerProperties.DATA.clear();
|
||||
ServerProperties.DATA.putAll(data);
|
||||
}
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to update server.properties main world to \"" + newName + "\"");
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static MainWorldPublication publishMainWorldFiles(
|
||||
Path currentLevelRoot,
|
||||
Path sourceDimensionRoot,
|
||||
Path targetLevelRoot
|
||||
) throws IOException {
|
||||
Path current = Objects.requireNonNull(currentLevelRoot, "currentLevelRoot").toAbsolutePath().normalize();
|
||||
Path sourceDimension = Objects.requireNonNull(sourceDimensionRoot, "sourceDimensionRoot").toAbsolutePath().normalize();
|
||||
Path target = Objects.requireNonNull(targetLevelRoot, "targetLevelRoot").toAbsolutePath().normalize();
|
||||
Path worldContainer = current.getParent();
|
||||
Path sourceNamespace = current.resolve("dimensions/iris");
|
||||
if (worldContainer == null || !Objects.equals(target.getParent(), worldContainer)) {
|
||||
throw new IOException("Promoted main world must be a direct child of the world container.");
|
||||
}
|
||||
if (!Objects.equals(sourceDimension.getParent(), sourceNamespace)) {
|
||||
throw new IOException("Promoted source must be a direct Iris dimension.");
|
||||
}
|
||||
if (Objects.equals(current, target)) {
|
||||
throw new IOException("Promoted main world cannot replace the current main world.");
|
||||
}
|
||||
if (Files.isSymbolicLink(worldContainer)
|
||||
|| Files.isSymbolicLink(current)
|
||||
|| !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Current world storage is missing or unsafe.");
|
||||
}
|
||||
if (Files.isSymbolicLink(sourceDimension)
|
||||
|| !Files.isDirectory(sourceDimension, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Promoted Iris dimension is missing or unsafe: " + sourceDimension);
|
||||
}
|
||||
requireAbsentMainWorldTarget(target);
|
||||
|
||||
Path stage = Files.createTempDirectory(worldContainer, "." + target.getFileName() + ".promoting-");
|
||||
boolean published = false;
|
||||
try {
|
||||
for (String subdirectory : List.of("data", "datapacks", "players")) {
|
||||
Path source = current.resolve(subdirectory);
|
||||
if (!Files.exists(source, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(source)) {
|
||||
continue;
|
||||
}
|
||||
copyWorldTree(source, stage.resolve(subdirectory));
|
||||
}
|
||||
|
||||
Path targetDimension = IrisWorldStorage.dimensionRoot(
|
||||
stage.toFile(),
|
||||
NamespacedKey.minecraft("overworld")
|
||||
).toPath();
|
||||
copyWorldTree(sourceDimension, targetDimension);
|
||||
requireAbsentMainWorldTarget(target);
|
||||
Files.move(stage, target);
|
||||
published = true;
|
||||
return new MainWorldPublication(target);
|
||||
} finally {
|
||||
if (!published) {
|
||||
AtomicDirectoryPublisher.deleteTree(stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireAbsentMainWorldTarget(Path target) throws IOException {
|
||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) {
|
||||
throw new FileAlreadyExistsException("Main-world target already exists: " + target);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyWorldTree(Path source, Path target) throws IOException {
|
||||
if (Files.isSymbolicLink(source)) {
|
||||
throw new IOException("World data contains a symbolic link: " + source);
|
||||
}
|
||||
if (Files.isRegularFile(source, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(target);
|
||||
} catch (IllegalArgumentException e) {
|
||||
sender().sendMessage(C.RED + e.getMessage());
|
||||
return;
|
||||
}
|
||||
if (!Files.isDirectory(source, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("World data contains an unsupported entry: " + source);
|
||||
}
|
||||
try (Stream<Path> entries = Files.walk(source)) {
|
||||
for (Path entry : entries.sorted(Comparator.naturalOrder()).toList()) {
|
||||
if (Files.isSymbolicLink(entry)) {
|
||||
throw new IOException("World data contains a symbolic link: " + entry);
|
||||
}
|
||||
Path destination = target.resolve(source.relativize(entry)).normalize();
|
||||
if (!destination.startsWith(target)) {
|
||||
throw new IOException("World data escapes its promotion stage: " + entry);
|
||||
}
|
||||
if (Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(destination);
|
||||
} else if (Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(destination.getParent());
|
||||
Files.copy(entry, destination, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
} else {
|
||||
throw new IOException("World data contains an unsupported entry: " + entry);
|
||||
}
|
||||
|
||||
String resolvedType = type.equalsIgnoreCase("default")
|
||||
? IrisSettings.get().getGenerator().getDefaultWorldType()
|
||||
: type;
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
|
||||
if (dimension == null) {
|
||||
sender().sendMessage("Could not find dimension '" + resolvedType + "'.");
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
|
||||
sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType)
|
||||
+ " and restart the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PendingWorldReplacementManager.StagedReplacement staged = Iris.instance
|
||||
.pendingWorldReplacements()
|
||||
.stageReplacement(sender(), worldKey, dimension);
|
||||
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||
+ ". Restart once to publish it. The current dimension is retained until Iris verifies the replacement.");
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||
? failure.getClass().getSimpleName()
|
||||
: failure.getMessage();
|
||||
sender().sendMessage(C.RED + "Could not stage the world replacement: " + detail);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeServerPropertiesAtomically(Path propertiesFile, Properties data) throws IOException {
|
||||
Path target = propertiesFile.toAbsolutePath().normalize();
|
||||
Path parent = target.getParent();
|
||||
if (parent == null) {
|
||||
throw new IOException("server.properties has no parent directory.");
|
||||
}
|
||||
Path stage = Files.createTempFile(parent, ".server.properties.promoting-", ".tmp");
|
||||
IOException operationFailure = null;
|
||||
try {
|
||||
try (FileOutputStream out = new FileOutputStream(stage.toFile())) {
|
||||
data.store(out, null);
|
||||
out.getFD().sync();
|
||||
}
|
||||
try {
|
||||
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(stage, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
operationFailure = e;
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(stage);
|
||||
} catch (IOException cleanupFailure) {
|
||||
if (operationFailure != null) {
|
||||
operationFailure.addSuppressed(cleanupFailure);
|
||||
} else {
|
||||
throw cleanupFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
|
||||
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed) {
|
||||
try {
|
||||
IrisStartupValidation.requireWorldCreationReady();
|
||||
PackValidationRegistry.requireLoadable(
|
||||
@@ -441,6 +251,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(name);
|
||||
LifecycleOperationCoordinator.Lease worldLease = null;
|
||||
File worldFolder = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
||||
Path stagedWorld = null;
|
||||
try {
|
||||
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||
worldLease = coordinator.acquire(
|
||||
@@ -459,53 +270,68 @@ public class CommandIris implements DirectorExecutor {
|
||||
sender().sendMessage(C.RED + "Failed to compile the Iris datapack. No world files were staged.");
|
||||
return false;
|
||||
}
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder);
|
||||
|
||||
Path targetWorld = worldFolder.toPath().toAbsolutePath().normalize();
|
||||
Path namespaceRoot = targetWorld.getParent();
|
||||
if (namespaceRoot == null) {
|
||||
throw new IOException("Iris world target has no namespace directory: " + targetWorld);
|
||||
}
|
||||
Files.createDirectories(namespaceRoot);
|
||||
stagedWorld = Files.createTempDirectory(namespaceRoot, ".iris-create-" + worldKey.getKey() + "-");
|
||||
Path sourceOverworld = IrisWorldStorage.dimensionRoot(
|
||||
IrisWorldStorage.levelRoot(),
|
||||
NamespacedKey.minecraft("overworld")
|
||||
).toPath();
|
||||
INMS.get().writeCurrentPaperWorldData(sourceOverworld, stagedWorld, seed);
|
||||
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
||||
sender(),
|
||||
dimension,
|
||||
stagedWorld.toFile()
|
||||
);
|
||||
if (installed == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
deleteDirectorySafely(worldFolder);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
|
||||
deleteDirectorySafely(worldFolder);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (main) {
|
||||
if (updateMainWorldUnderLease(name)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_UPDATED_SERVER_PROPERTIES_LEVEL_NAME, MessageArgument.untrusted("name", name)));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_WAS_STAGED_BUT_FAILED_UPDATE_SERVER_PROPERTIES_MAIN_WORLD));
|
||||
try {
|
||||
BukkitWorldConfiguration.remove(BUKKIT_YML, name);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError("Failed to roll back bukkit.yml after main-world staging failed.", e);
|
||||
}
|
||||
deleteDirectorySafely(worldFolder);
|
||||
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publishAbsent(
|
||||
stagedWorld,
|
||||
targetWorld
|
||||
)) {
|
||||
stagedWorld = null;
|
||||
if (!registerWorldInBukkitYml(worldKey, dimension.getLoadKey(), seed)) {
|
||||
return false;
|
||||
}
|
||||
publication.commit();
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED, MessageArgument.untrusted("name", name), MessageArgument.untrusted("value", dimension.getLoadKey()), MessageArgument.untrusted("seed", seed)));
|
||||
if (main) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_WORLD_IS_NOW_CONFIGURED_AS_MAIN_NEXT_RESTART));
|
||||
}
|
||||
return true;
|
||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||
sender().sendMessage(C.YELLOW + e.getMessage());
|
||||
return false;
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Failed to stage the complete Iris world: " + e.getMessage());
|
||||
Iris.reportError("Failed to stage complete Folia world \"" + worldKey + "\".", e);
|
||||
return false;
|
||||
} finally {
|
||||
if (stagedWorld != null) {
|
||||
deleteDirectorySafely(stagedWorld.toFile());
|
||||
}
|
||||
if (worldLease != null) {
|
||||
worldLease.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
|
||||
String logicalWorldName = IrisWorldStorage.logicalName(IrisWorldStorage.keyFromName(worldName));
|
||||
private boolean registerWorldInBukkitYml(NamespacedKey worldKey, String dimension, Long seed) {
|
||||
String configuredWorldName = IrisWorldStorage.configuredWorldName(
|
||||
worldKey,
|
||||
IrisWorldStorage.levelRoot().getName()
|
||||
);
|
||||
try {
|
||||
BukkitWorldConfiguration.register(BUKKIT_YML, logicalWorldName, dimension, seed);
|
||||
Iris.info("Registered \"" + logicalWorldName + "\" in bukkit.yml");
|
||||
BukkitWorldConfiguration.register(BUKKIT_YML, configuredWorldName, dimension, seed);
|
||||
Iris.info("Registered \"" + configuredWorldName + "\" in bukkit.yml");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
@@ -1097,31 +923,4 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
static final class MainWorldPublication implements AutoCloseable {
|
||||
private final Path target;
|
||||
private boolean committed;
|
||||
private boolean closed;
|
||||
|
||||
MainWorldPublication(Path target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
void commit() {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Main-world publication is already closed.");
|
||||
}
|
||||
committed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
if (!committed) {
|
||||
AtomicDirectoryPublisher.deleteTree(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -378,6 +378,16 @@ public class BukkitWorldReconcilerTest {
|
||||
return configuredSeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String configuredWorldName(NamespacedKey requestedWorldKey) {
|
||||
return requestedWorldKey.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NamespacedKey worldKeyFromConfiguration(String configuredWorldName) {
|
||||
return worldKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<World> loadedWorld(NamespacedKey requestedWorldKey) {
|
||||
return worldKey.equals(requestedWorldKey) ? loaded : Optional.empty();
|
||||
|
||||
+13
@@ -7,6 +7,7 @@ import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.bukkit.NamespacedKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -63,6 +64,18 @@ public class IrisWorldGeneratorResolverTest {
|
||||
assertFalse(invalid.getBlockingErrors().toString(), invalid.isLoadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paperStartupAliasResolvesToCanonicalRuntimeKey() {
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "moon"),
|
||||
IrisWorldGeneratorResolver.configuredWorldKey("world_iris_moon", "world")
|
||||
);
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "moon"),
|
||||
IrisWorldGeneratorResolver.configuredWorldKey("moon", "world")
|
||||
);
|
||||
}
|
||||
|
||||
private static void writeValidPack(Path packRoot) throws Exception {
|
||||
Files.createDirectories(packRoot.resolve("dimensions"));
|
||||
Files.createDirectories(packRoot.resolve("regions"));
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy.SlotKind;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEnvironment;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class PendingWorldReplacementManagerPolicyTest {
|
||||
@After
|
||||
public void disableValidation() {
|
||||
IrisStartupValidation.disable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void irisManagedSlotsAllowEveryReplacementEnvironment() {
|
||||
for (IrisEnvironment environment : IrisEnvironment.values()) {
|
||||
PendingWorldReplacementManager.requireCompatibleEnvironment(SlotKind.IRIS_MANAGED, environment);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaSlotsRequireTheirIdentityEnvironment() {
|
||||
List<EnvironmentExpectation> expectations = List.of(
|
||||
new EnvironmentExpectation(SlotKind.VANILLA_OVERWORLD, IrisEnvironment.NORMAL),
|
||||
new EnvironmentExpectation(SlotKind.VANILLA_NETHER, IrisEnvironment.NETHER),
|
||||
new EnvironmentExpectation(SlotKind.VANILLA_END, IrisEnvironment.THE_END)
|
||||
);
|
||||
|
||||
for (EnvironmentExpectation expectation : expectations) {
|
||||
for (IrisEnvironment environment : IrisEnvironment.values()) {
|
||||
if (environment == expectation.environment()) {
|
||||
PendingWorldReplacementManager.requireCompatibleEnvironment(
|
||||
expectation.slotKind(),
|
||||
environment
|
||||
);
|
||||
continue;
|
||||
}
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PendingWorldReplacementManager.requireCompatibleEnvironment(
|
||||
expectation.slotKind(),
|
||||
environment
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledVanillaSlotsFailWithoutRestrictingOtherSlots() throws Exception {
|
||||
PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.IRIS_MANAGED, false, false);
|
||||
PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.VANILLA_OVERWORLD, false, false);
|
||||
PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.VANILLA_NETHER, true, false);
|
||||
PendingWorldReplacementManager.requireVanillaSlotEnabled(SlotKind.VANILLA_END, false, true);
|
||||
|
||||
IOException netherFailure = assertThrows(
|
||||
IOException.class,
|
||||
() -> PendingWorldReplacementManager.requireVanillaSlotEnabled(
|
||||
SlotKind.VANILLA_NETHER,
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
IOException endFailure = assertThrows(
|
||||
IOException.class,
|
||||
() -> PendingWorldReplacementManager.requireVanillaSlotEnabled(
|
||||
SlotKind.VANILLA_END,
|
||||
true,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"allow-nether must be true before the vanilla Nether can be replaced.",
|
||||
netherFailure.getMessage()
|
||||
);
|
||||
assertEquals(
|
||||
"Bukkit allow-end must be true before the vanilla End can be replaced.",
|
||||
endFailure.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementStagingPassesTheRestartBoundaryWithoutUnlockingCreation() {
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.markDatapacksReady();
|
||||
IrisStartupValidation.markPacksReady();
|
||||
IrisStartupValidation.requireRestart("restart boundary");
|
||||
PendingWorldReplacementManager manager = new PendingWorldReplacementManager(mock(Iris.class));
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
|
||||
assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady);
|
||||
try (MockedStatic<WorldReplacementBootstrapMarker> bootstrapMarker =
|
||||
mockStatic(WorldReplacementBootstrapMarker.class)) {
|
||||
bootstrapMarker.when(WorldReplacementBootstrapMarker::wasBootstrappedThisProcess).thenReturn(false);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> manager.stageReplacement(sender, NamespacedKey.minecraft("the_nether"), dimension)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"Exact world replacement requires a full Paper-family startup bootstrap.",
|
||||
failure.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private record EnvironmentExpectation(SlotKind slotKind, IrisEnvironment environment) {
|
||||
}
|
||||
}
|
||||
+25
-10
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -8,28 +9,42 @@ import java.lang.reflect.Parameter;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CommandIrisCreateOverwriteContractTest {
|
||||
@Test
|
||||
public void createExposesOptInRestartReplacementFlag() throws Exception {
|
||||
public void createOnlyAcceptsNameTypeAndSeed() throws Exception {
|
||||
Method command = CommandIris.class.getDeclaredMethod(
|
||||
"create",
|
||||
String.class,
|
||||
String.class,
|
||||
long.class,
|
||||
boolean.class,
|
||||
boolean.class
|
||||
long.class
|
||||
);
|
||||
Parameter overwriteParameter = command.getParameters()[4];
|
||||
Param overwrite = overwriteParameter.getAnnotation(Param.class);
|
||||
|
||||
assertEquals("overwrite", overwrite.name());
|
||||
assertEquals("false", overwrite.defaultValue());
|
||||
assertTrue(Arrays.asList(overwrite.aliases()).contains("force"));
|
||||
assertEquals(3, command.getParameterCount());
|
||||
assertFalse(Arrays.stream(command.getParameterTypes()).anyMatch(type -> type == boolean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replaceOwnsOverrideAndOverwriteAliasesWithoutASeed() throws Exception {
|
||||
Method command = CommandIris.class.getDeclaredMethod("replace", String.class, String.class);
|
||||
Director director = command.getAnnotation(Director.class);
|
||||
Parameter targetParameter = command.getParameters()[0];
|
||||
Param target = targetParameter.getAnnotation(Param.class);
|
||||
Parameter typeParameter = command.getParameters()[1];
|
||||
Param type = typeParameter.getAnnotation(Param.class);
|
||||
|
||||
assertTrue(Arrays.asList(director.aliases()).contains("override"));
|
||||
assertTrue(Arrays.asList(director.aliases()).contains("overwrite"));
|
||||
assertEquals(
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
|
||||
overwrite.descriptionKey()
|
||||
director.descriptionKey()
|
||||
);
|
||||
assertEquals("target", target.name());
|
||||
assertEquals(director.descriptionKey(), target.descriptionKey());
|
||||
assertEquals("default", type.defaultValue());
|
||||
assertEquals(CommandIris.PackDimensionTypeHandler.class, type.customHandler());
|
||||
assertFalse(Arrays.stream(command.getParameterTypes()).anyMatch(parameterType -> parameterType == long.class));
|
||||
}
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CommandIrisFoliaCreateContractTest {
|
||||
@Test
|
||||
public void ordinaryFoliaCreateRestartsOnlyAfterSuccessfulStagingAndFeedback() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource")));
|
||||
String foliaCreate = source.substring(
|
||||
source.indexOf("if (J.isFolia()) {"),
|
||||
source.indexOf(" try {", source.indexOf("if (J.isFolia()) {"))
|
||||
);
|
||||
|
||||
int stage = foliaCreate.indexOf("stageFoliaWorldCreation(worldName, dimension, seed)");
|
||||
int failureExit = foliaCreate.indexOf("if (!staged)");
|
||||
int feedback = foliaCreate.indexOf("COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD");
|
||||
int restart = foliaCreate.indexOf("ServerConfigurator.restart(\"Iris staged Folia world");
|
||||
|
||||
assertTrue(stage >= 0);
|
||||
assertTrue(stage < failureExit);
|
||||
assertTrue(failureExit < feedback);
|
||||
assertTrue(feedback < restart);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foliaStagePublishesCurrentPaperDataBeforeRegisteringStartupAlias() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource")));
|
||||
int methodStart = source.indexOf("private boolean stageFoliaWorldCreation(");
|
||||
int methodEnd = source.indexOf("private boolean registerWorldInBukkitYml(", methodStart);
|
||||
String staging = source.substring(methodStart, methodEnd);
|
||||
|
||||
int currentPaperData = staging.indexOf("INMS.get().writeCurrentPaperWorldData(");
|
||||
int pack = staging.indexOf("installIntoWorld(");
|
||||
int publication = staging.indexOf("AtomicDirectoryPublisher.publishAbsent(");
|
||||
int registration = staging.indexOf("registerWorldInBukkitYml(worldKey");
|
||||
|
||||
assertTrue(currentPaperData >= 0);
|
||||
assertTrue(currentPaperData < pack);
|
||||
assertTrue(pack < publication);
|
||||
assertTrue(publication < registration);
|
||||
assertTrue(source.contains("IrisWorldStorage.configuredWorldName("));
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CommandIrisMainWorldPromotionTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void existingTopLevelWorldIsRefusedWithoutMerging() throws IOException {
|
||||
PromotionPaths paths = createPromotionPaths("existing-target");
|
||||
Files.createDirectories(paths.target());
|
||||
Files.writeString(paths.target().resolve("sentinel.txt"), "keep");
|
||||
|
||||
assertThrows(FileAlreadyExistsException.class, () -> CommandIris.publishMainWorldFiles(
|
||||
paths.current(),
|
||||
paths.sourceDimension(),
|
||||
paths.target()
|
||||
));
|
||||
|
||||
assertEquals("keep", Files.readString(paths.target().resolve("sentinel.txt")));
|
||||
assertFalse(Files.exists(paths.target().resolve("dimensions/minecraft/overworld/region/r.0.0.mca")));
|
||||
assertFalse(hasPromotionStage(paths.root(), paths.target().getFileName().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncommittedPromotionRollsBackThePublishedWorld() throws IOException {
|
||||
PromotionPaths paths = createPromotionPaths("rollback-target");
|
||||
|
||||
try (CommandIris.MainWorldPublication publication = CommandIris.publishMainWorldFiles(
|
||||
paths.current(),
|
||||
paths.sourceDimension(),
|
||||
paths.target()
|
||||
)) {
|
||||
assertTrue(Files.isRegularFile(paths.target().resolve("data/map.dat")));
|
||||
assertTrue(Files.isRegularFile(paths.target().resolve("dimensions/minecraft/overworld/region/r.0.0.mca")));
|
||||
}
|
||||
|
||||
assertFalse(Files.exists(paths.target()));
|
||||
assertFalse(hasPromotionStage(paths.root(), paths.target().getFileName().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void committedPromotionKeepsTheCompleteStagedWorld() throws IOException {
|
||||
PromotionPaths paths = createPromotionPaths("committed-target");
|
||||
|
||||
try (CommandIris.MainWorldPublication publication = CommandIris.publishMainWorldFiles(
|
||||
paths.current(),
|
||||
paths.sourceDimension(),
|
||||
paths.target()
|
||||
)) {
|
||||
publication.commit();
|
||||
}
|
||||
|
||||
assertEquals("map", Files.readString(paths.target().resolve("data/map.dat")));
|
||||
assertEquals("region", Files.readString(paths.target().resolve("dimensions/minecraft/overworld/region/r.0.0.mca")));
|
||||
assertFalse(hasPromotionStage(paths.root(), paths.target().getFileName().toString()));
|
||||
}
|
||||
|
||||
private PromotionPaths createPromotionPaths(String targetName) throws IOException {
|
||||
Path root = temporaryFolder.newFolder(targetName + "-root").toPath();
|
||||
Path current = root.resolve("world");
|
||||
Path sourceDimension = current.resolve("dimensions/iris/" + targetName);
|
||||
Path target = root.resolve(targetName);
|
||||
Files.createDirectories(current.resolve("data"));
|
||||
Files.writeString(current.resolve("data/map.dat"), "map");
|
||||
Files.createDirectories(sourceDimension.resolve("region"));
|
||||
Files.writeString(sourceDimension.resolve("region/r.0.0.mca"), "region");
|
||||
return new PromotionPaths(root, current, sourceDimension, target);
|
||||
}
|
||||
|
||||
private boolean hasPromotionStage(Path root, String targetName) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(root)) {
|
||||
return entries.anyMatch(path -> path.getFileName().toString().startsWith("." + targetName + ".promoting-"));
|
||||
}
|
||||
}
|
||||
|
||||
private record PromotionPaths(Path root, Path current, Path sourceDimension, Path target) {
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,8 @@ nmsBindings.each { key, value ->
|
||||
tasks.withType(org.gradle.api.tasks.testing.Test).configureEach {
|
||||
systemProperty('iris.nmsChunkGeneratorSource',
|
||||
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/IrisChunkGenerator.java").absolutePath)
|
||||
systemProperty('iris.nmsBindingSource',
|
||||
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/NMSBinding.java").absolutePath)
|
||||
systemProperty('iris.nativeStructurePostProcessorSource',
|
||||
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath)
|
||||
systemProperty('iris.nativeStructureStartInjectorSource',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.BuildConstants;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
@@ -21,6 +22,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -37,9 +39,29 @@ import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class IrisDatapackCompiler {
|
||||
private static final int INPUT_FINGERPRINT_SCHEMA = 1;
|
||||
private static final int INPUT_FINGERPRINT_SCHEMA = 2;
|
||||
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
||||
private static final List<String> INPUT_DIRECTORIES = List.of("dimensions", "biomes", "snippet");
|
||||
private static final String FLAT_VOID_LEVEL_STEM = """
|
||||
{
|
||||
"type": "%s",
|
||||
"generator": {
|
||||
"type": "minecraft:flat",
|
||||
"settings": {
|
||||
"biome": "minecraft:the_void",
|
||||
"features": false,
|
||||
"lakes": false,
|
||||
"layers": [
|
||||
{
|
||||
"block": "minecraft:air",
|
||||
"height": 1
|
||||
}
|
||||
],
|
||||
"structure_overrides": []
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private IrisDatapackCompiler() {
|
||||
}
|
||||
@@ -65,28 +87,37 @@ public final class IrisDatapackCompiler {
|
||||
|
||||
public static String computeInputFingerprint(
|
||||
List<File> packRoots,
|
||||
List<IrisGeneratorBinding> bindings,
|
||||
IDataFixer fixer,
|
||||
boolean adjustVanillaHeight
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(fixer, "fixer");
|
||||
return computeInputFingerprint(
|
||||
packRoots,
|
||||
bindings,
|
||||
adjustVanillaHeight,
|
||||
compilerIdentity(fixer));
|
||||
}
|
||||
|
||||
static String computeInputFingerprint(
|
||||
List<File> packRoots,
|
||||
List<IrisGeneratorBinding> bindings,
|
||||
boolean adjustVanillaHeight,
|
||||
String compilerIdentity
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(packRoots, "packRoots");
|
||||
Objects.requireNonNull(compilerIdentity, "compilerIdentity");
|
||||
List<IrisGeneratorBinding> normalizedBindings = normalizeBindings(bindings);
|
||||
MessageDigest digest = sha256();
|
||||
updateDigestString(digest, "iris-datapack-compiler-input");
|
||||
updateDigestInt(digest, INPUT_FINGERPRINT_SCHEMA);
|
||||
updateDigestString(digest, compilerIdentity);
|
||||
digest.update((byte) (adjustVanillaHeight ? 1 : 0));
|
||||
updateDigestInt(digest, normalizedBindings.size());
|
||||
for (IrisGeneratorBinding binding : normalizedBindings) {
|
||||
updateDigestString(digest, binding.worldKey().toString());
|
||||
updateDigestString(digest, binding.dimension());
|
||||
}
|
||||
updateDigestInt(digest, packRoots.size());
|
||||
|
||||
for (int index = 0; index < packRoots.size(); index++) {
|
||||
@@ -139,12 +170,14 @@ public final class IrisDatapackCompiler {
|
||||
public static CompilationResult compile(
|
||||
List<File> packRoots,
|
||||
KList<File> datapackRoots,
|
||||
List<IrisGeneratorBinding> bindings,
|
||||
IDataFixer fixer,
|
||||
boolean adjustVanillaHeight
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(packRoots, "packRoots");
|
||||
Objects.requireNonNull(datapackRoots, "datapackRoots");
|
||||
Objects.requireNonNull(fixer, "fixer");
|
||||
List<IrisGeneratorBinding> normalizedBindings = normalizeBindings(bindings);
|
||||
if (datapackRoots.isEmpty()) {
|
||||
throw new IOException("No Iris datapack output roots were provided");
|
||||
}
|
||||
@@ -154,6 +187,7 @@ public final class IrisDatapackCompiler {
|
||||
|
||||
DimensionHeight height = new DimensionHeight(fixer);
|
||||
Map<String, KSet<String>> biomes = new LinkedHashMap<>();
|
||||
Map<String, List<DimensionCandidate>> dimensions = new LinkedHashMap<>();
|
||||
int packCount = 0;
|
||||
int dimensionCount = 0;
|
||||
for (File packRoot : packRoots) {
|
||||
@@ -177,6 +211,12 @@ public final class IrisDatapackCompiler {
|
||||
}
|
||||
IrisLogging.debug(" Compiling Dimension " + dimension.getLoadFile().getPath());
|
||||
height.merge(dimension);
|
||||
dimensions.computeIfAbsent(dimension.getLoadKey(), ignored -> new ArrayList<>())
|
||||
.add(new DimensionCandidate(
|
||||
dimension,
|
||||
packRoot.toPath().toAbsolutePath().normalize(),
|
||||
dimension.getDimensionType().toJson(fixer)
|
||||
));
|
||||
KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
|
||||
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes);
|
||||
dimension.installDimensionType(fixer, datapackRoots);
|
||||
@@ -192,10 +232,80 @@ public final class IrisDatapackCompiler {
|
||||
}
|
||||
|
||||
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
|
||||
validateOutputs(datapackRoots, dimensionCount);
|
||||
installLevelStemBindings(normalizedBindings, dimensions, datapackRoots);
|
||||
validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size());
|
||||
return new CompilationResult(packCount, dimensionCount, countBiomes(biomes));
|
||||
}
|
||||
|
||||
private static void installLevelStemBindings(
|
||||
List<IrisGeneratorBinding> bindings,
|
||||
Map<String, List<DimensionCandidate>> dimensions,
|
||||
Collection<File> datapackRoots
|
||||
) throws IOException {
|
||||
for (IrisGeneratorBinding binding : bindings) {
|
||||
DimensionCandidate selected = resolveDimension(binding, dimensions.get(binding.dimension()));
|
||||
String typeKey = "iris:" + selected.dimension().getDimensionTypeKey();
|
||||
String levelStem = FLAT_VOID_LEVEL_STEM.formatted(typeKey);
|
||||
for (File datapackRoot : datapackRoots) {
|
||||
Path output = datapackRoot.toPath()
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.resolve("data/iris/dimension")
|
||||
.resolve(binding.worldKey().key() + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(
|
||||
output,
|
||||
levelStem,
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE_NEW,
|
||||
StandardOpenOption.WRITE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DimensionCandidate resolveDimension(
|
||||
IrisGeneratorBinding binding,
|
||||
List<DimensionCandidate> candidates
|
||||
) throws IOException {
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
throw new IOException("Iris world " + binding.worldKey() + " selects missing dimension \""
|
||||
+ binding.dimension() + "\".");
|
||||
}
|
||||
DimensionCandidate selected = candidates.getFirst();
|
||||
for (int index = 1; index < candidates.size(); index++) {
|
||||
DimensionCandidate candidate = candidates.get(index);
|
||||
if (!selected.dimensionTypeJson().equals(candidate.dimensionTypeJson())) {
|
||||
throw new IOException("Iris world " + binding.worldKey() + " selects ambiguous dimension \""
|
||||
+ binding.dimension() + "\" from " + selected.packRoot() + " and "
|
||||
+ candidate.packRoot() + ".");
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static List<IrisGeneratorBinding> normalizeBindings(List<IrisGeneratorBinding> bindings)
|
||||
throws IOException {
|
||||
List<IrisGeneratorBinding> requiredBindings = List.copyOf(
|
||||
Objects.requireNonNull(bindings, "bindings")
|
||||
);
|
||||
Map<WorldSlotKey, IrisGeneratorBinding> byWorld = new LinkedHashMap<>();
|
||||
for (IrisGeneratorBinding binding : requiredBindings) {
|
||||
IrisGeneratorBinding requiredBinding = Objects.requireNonNull(binding, "binding");
|
||||
IrisGeneratorBinding previous = byWorld.putIfAbsent(
|
||||
requiredBinding.worldKey(),
|
||||
requiredBinding
|
||||
);
|
||||
if (previous != null) {
|
||||
throw new IOException("Multiple Iris LevelStem bindings target "
|
||||
+ requiredBinding.worldKey() + ".");
|
||||
}
|
||||
}
|
||||
ArrayList<IrisGeneratorBinding> normalized = new ArrayList<>(byWorld.values());
|
||||
normalized.sort(Comparator.comparing(binding -> binding.worldKey().toString()));
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
private static void collectInstalledPackRoots(
|
||||
Path packsRoot,
|
||||
Map<Path, File> roots,
|
||||
@@ -363,7 +473,11 @@ public final class IrisDatapackCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateOutputs(Collection<File> datapackRoots, int dimensionCount) throws IOException {
|
||||
private static void validateOutputs(
|
||||
Collection<File> datapackRoots,
|
||||
int dimensionCount,
|
||||
int levelStemCount
|
||||
) throws IOException {
|
||||
for (File datapackRoot : datapackRoots) {
|
||||
Path root = datapackRoot.toPath();
|
||||
if (!Files.isRegularFile(root.resolve("pack.mcmeta"))) {
|
||||
@@ -372,6 +486,9 @@ public final class IrisDatapackCompiler {
|
||||
if (dimensionCount > 0 && !Files.isDirectory(root.resolve("data/iris/dimension_type"))) {
|
||||
throw new IOException("Iris dimension types were not generated at " + root);
|
||||
}
|
||||
if (levelStemCount > 0 && !Files.isDirectory(root.resolve("data/iris/dimension"))) {
|
||||
throw new IOException("Iris LevelStem bindings were not generated at " + root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +506,13 @@ public final class IrisDatapackCompiler {
|
||||
private record CompilerInputEntry(Path source, String relativePath) {
|
||||
}
|
||||
|
||||
private record DimensionCandidate(
|
||||
IrisDimension dimension,
|
||||
Path packRoot,
|
||||
String dimensionTypeJson
|
||||
) {
|
||||
}
|
||||
|
||||
public static final class DimensionHeight {
|
||||
private final IDataFixer fixer;
|
||||
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
||||
|
||||
@@ -109,6 +109,16 @@ public final class IrisStartupValidation {
|
||||
}
|
||||
}
|
||||
|
||||
public static void requireWorldReplacementStagingReady() {
|
||||
Snapshot current = snapshot;
|
||||
if (current.enforced()
|
||||
&& current.datapacks() == ValidationState.RESTART_REQUIRED
|
||||
&& current.packs() == ValidationState.READY) {
|
||||
return;
|
||||
}
|
||||
requireWorldCreationReady();
|
||||
}
|
||||
|
||||
static Snapshot snapshot() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -125,6 +126,54 @@ public final class IrisWorldStorage {
|
||||
return key;
|
||||
}
|
||||
|
||||
public static NamespacedKey replacementKeyFromName(String requestedName, String levelName) {
|
||||
String requested = Objects.requireNonNull(requestedName, "requestedName").trim();
|
||||
String configuredLevelName = Objects.requireNonNull(levelName, "levelName").trim();
|
||||
if (requested.isEmpty()) {
|
||||
throw new IllegalArgumentException("World name cannot be empty.");
|
||||
}
|
||||
if (configuredLevelName.isEmpty()) {
|
||||
throw new IllegalArgumentException("Configured level name cannot be empty.");
|
||||
}
|
||||
if (requested.contains("/") || requested.contains("\\") || requested.contains("..")) {
|
||||
throw new IllegalArgumentException("World name must be a safe single path segment.");
|
||||
}
|
||||
|
||||
String normalized = requested.toLowerCase(Locale.ENGLISH);
|
||||
if (normalized.contains(":")) {
|
||||
int separator = normalized.indexOf(':');
|
||||
if (separator == 0
|
||||
|| separator == normalized.length() - 1
|
||||
|| separator != normalized.lastIndexOf(':')) {
|
||||
throw new IllegalArgumentException("World identifier is invalid: " + requestedName);
|
||||
}
|
||||
NamespacedKey explicit = NamespacedKey.fromString(normalized);
|
||||
if (explicit == null) {
|
||||
throw new IllegalArgumentException("World identifier is invalid: " + requestedName);
|
||||
}
|
||||
return explicit;
|
||||
}
|
||||
String configuredIrisPrefix = configuredLevelName + "_" + IRIS_NAMESPACE + "_";
|
||||
if (requested.startsWith(configuredIrisPrefix)) {
|
||||
return keyFromConfiguredWorldName(requested, configuredLevelName);
|
||||
}
|
||||
if (requested.equalsIgnoreCase(configuredLevelName)) {
|
||||
return NamespacedKey.minecraft("overworld");
|
||||
}
|
||||
if (requested.equalsIgnoreCase(configuredLevelName + "_nether")) {
|
||||
return NamespacedKey.minecraft("the_nether");
|
||||
}
|
||||
if (requested.equalsIgnoreCase(configuredLevelName + "_the_end")) {
|
||||
return NamespacedKey.minecraft("the_end");
|
||||
}
|
||||
return switch (normalized) {
|
||||
case "main", "overworld" -> NamespacedKey.minecraft("overworld");
|
||||
case "nether", "the_nether" -> NamespacedKey.minecraft("the_nether");
|
||||
case "end", "the_end" -> NamespacedKey.minecraft("the_end");
|
||||
default -> new NamespacedKey(IRIS_NAMESPACE, normalized.replace(' ', '_'));
|
||||
};
|
||||
}
|
||||
|
||||
static NamespacedKey keyFromName(String worldName, String levelName) {
|
||||
String name = Objects.requireNonNull(worldName, "worldName").trim();
|
||||
String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim();
|
||||
@@ -145,6 +194,53 @@ public final class IrisWorldStorage {
|
||||
return new NamespacedKey(IRIS_NAMESPACE, key);
|
||||
}
|
||||
|
||||
public static NamespacedKey keyFromConfiguredWorldName(String configuredWorldName, String levelName) {
|
||||
String name = Objects.requireNonNull(configuredWorldName, "configuredWorldName").trim();
|
||||
String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim();
|
||||
if (name.isEmpty()) {
|
||||
throw new IllegalArgumentException("Configured world name cannot be empty.");
|
||||
}
|
||||
if (mainLevelName.isEmpty()) {
|
||||
throw new IllegalArgumentException("Level name cannot be empty.");
|
||||
}
|
||||
|
||||
String irisPrefix = mainLevelName + "_" + IRIS_NAMESPACE + "_";
|
||||
if (name.startsWith(irisPrefix)) {
|
||||
return managedKeyFromName(IRIS_NAMESPACE + ":" + name.substring(irisPrefix.length()), mainLevelName);
|
||||
}
|
||||
return keyFromName(name, mainLevelName);
|
||||
}
|
||||
|
||||
public static String configuredWorldName(NamespacedKey key, String levelName) {
|
||||
NamespacedKey worldKey = Objects.requireNonNull(key, "key");
|
||||
String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim();
|
||||
if (mainLevelName.isEmpty()) {
|
||||
throw new IllegalArgumentException("Level name cannot be empty.");
|
||||
}
|
||||
if (NamespacedKey.minecraft("overworld").equals(worldKey)) {
|
||||
return mainLevelName;
|
||||
}
|
||||
if (NamespacedKey.minecraft("the_nether").equals(worldKey)) {
|
||||
return mainLevelName + "_nether";
|
||||
}
|
||||
if (NamespacedKey.minecraft("the_end").equals(worldKey)) {
|
||||
return mainLevelName + "_the_end";
|
||||
}
|
||||
if (!IRIS_NAMESPACE.equals(worldKey.getNamespace()) || !worldKey.getKey().matches("[a-z0-9_-]+")) {
|
||||
throw new IllegalArgumentException("Only safe Iris-managed world keys have a Bukkit startup name.");
|
||||
}
|
||||
return mainLevelName + "_" + worldKey.getNamespace() + "_" + worldKey.getKey();
|
||||
}
|
||||
|
||||
public static String configuredWorldName(WorldSlotKey key, String levelName) {
|
||||
WorldSlotKey worldKey = Objects.requireNonNull(key, "key");
|
||||
NamespacedKey namespacedKey = NamespacedKey.fromString(worldKey.toString());
|
||||
if (namespacedKey == null) {
|
||||
throw new IllegalArgumentException("World key is invalid: " + worldKey);
|
||||
}
|
||||
return configuredWorldName(namespacedKey, levelName);
|
||||
}
|
||||
|
||||
public static String logicalName(WorldInfo world) {
|
||||
return logicalName(WorldIdentity.key(world));
|
||||
}
|
||||
@@ -214,6 +310,15 @@ public final class IrisWorldStorage {
|
||||
return target.toFile();
|
||||
}
|
||||
|
||||
public static boolean isExistingManagedDimensionRoot(File levelRoot, NamespacedKey key) {
|
||||
try {
|
||||
Path target = requireSafeManagedDimensionRoot(levelRoot, key).toPath();
|
||||
return Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static File dimensionRoot(File levelRoot, NamespacedKey key) {
|
||||
Path dimensionsRoot = Objects.requireNonNull(levelRoot, "levelRoot")
|
||||
.toPath()
|
||||
|
||||
@@ -14,6 +14,7 @@ import art.arcane.volmlib.util.io.IO;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
@@ -25,9 +26,11 @@ import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -35,34 +38,41 @@ public class IrisWorlds {
|
||||
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType();
|
||||
private final Path levelRoot;
|
||||
private final Path registryFile;
|
||||
private final KMap<String, String> worlds;
|
||||
private volatile boolean dirty = false;
|
||||
|
||||
private IrisWorlds(KMap<String, String> worlds) {
|
||||
private IrisWorlds(Path levelRoot, KMap<String, String> worlds) {
|
||||
this.levelRoot = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize();
|
||||
registryFile = registryFile(this.levelRoot);
|
||||
this.worlds = new KMap<>();
|
||||
worlds.forEach((identity, type) -> this.worlds.put(WorldIdentity.parse(identity).toString(), type));
|
||||
readBukkitWorlds().forEach((name, type) -> put0(IrisWorldStorage.keyFromName(name).toString(), type));
|
||||
readBukkitWorlds(this.levelRoot).forEach((name, type) -> put0(
|
||||
IrisWorldStorage.keyFromConfiguredWorldName(name, this.levelRoot.getFileName().toString()).toString(),
|
||||
type));
|
||||
save();
|
||||
}
|
||||
|
||||
public static IrisWorlds get() {
|
||||
return cache.aquire(() -> {
|
||||
File file = IrisPlatforms.get().dataFile("worlds.json");
|
||||
Path levelRoot = IrisWorldStorage.levelRoot().toPath().toAbsolutePath().normalize();
|
||||
File file = registryFile(levelRoot).toFile();
|
||||
if (!file.exists()) {
|
||||
return new IrisWorlds(new KMap<>());
|
||||
return new IrisWorlds(levelRoot, new KMap<>());
|
||||
}
|
||||
|
||||
try {
|
||||
String json = IO.readAll(file);
|
||||
KMap<String, String> worlds = GSON.fromJson(json, TYPE);
|
||||
return new IrisWorlds(Objects.requireNonNullElseGet(worlds, KMap::new));
|
||||
return new IrisWorlds(levelRoot, Objects.requireNonNullElseGet(worlds, KMap::new));
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Failed to load worlds.json!");
|
||||
IrisLogging.error("Failed to load worlds.json for level root " + levelRoot + "!");
|
||||
e.printStackTrace();
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
|
||||
return new IrisWorlds(new KMap<>());
|
||||
return new IrisWorlds(levelRoot, new KMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,7 +124,9 @@ public class IrisWorlds {
|
||||
public synchronized KMap<String, String> getWorlds() {
|
||||
clean();
|
||||
KMap<String, String> result = new KMap<>();
|
||||
readBukkitWorlds().forEach((name, type) -> result.put(IrisWorldStorage.keyFromName(name).toString(), type));
|
||||
readBukkitWorlds(levelRoot).forEach((name, type) -> result.put(
|
||||
IrisWorldStorage.keyFromConfiguredWorldName(name, levelRoot.getFileName().toString()).toString(),
|
||||
type));
|
||||
return result.put(worlds);
|
||||
}
|
||||
|
||||
@@ -135,7 +147,7 @@ public class IrisWorlds {
|
||||
public synchronized void clean() {
|
||||
boolean removed = worlds.entrySet().removeIf(entry -> {
|
||||
try {
|
||||
File packRoot = IrisWorldStorage.packRoot(WorldIdentity.parse(entry.getKey()));
|
||||
File packRoot = packRoot(entry.getKey());
|
||||
return !new File(packRoot, "dimensions/" + entry.getValue() + ".json").exists();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return true;
|
||||
@@ -159,7 +171,7 @@ public class IrisWorlds {
|
||||
return;
|
||||
}
|
||||
|
||||
Path target = IrisPlatforms.get().dataFile("worlds.json").toPath().toAbsolutePath().normalize();
|
||||
Path target = registryFile;
|
||||
Path parent = target.getParent();
|
||||
if (parent == null) {
|
||||
throw new IOException("worlds.json target has no parent: " + target);
|
||||
@@ -193,6 +205,49 @@ public class IrisWorlds {
|
||||
}
|
||||
|
||||
public static KMap<String, String> readBukkitWorlds() {
|
||||
return readBukkitWorlds(IrisWorldStorage.levelRoot().toPath());
|
||||
}
|
||||
|
||||
static Path registryFile(Path levelRoot) {
|
||||
return Objects.requireNonNull(levelRoot, "levelRoot")
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.resolve("iris")
|
||||
.resolve("worlds.json");
|
||||
}
|
||||
|
||||
static KMap<String, String> filterBukkitWorldsByStorage(
|
||||
Path levelRoot,
|
||||
Map<String, String> configuredWorlds
|
||||
) {
|
||||
Path root = Objects.requireNonNull(levelRoot, "levelRoot").toAbsolutePath().normalize();
|
||||
Path levelNamePath = root.getFileName();
|
||||
if (levelNamePath == null) {
|
||||
throw new IllegalArgumentException("Selected level root has no name: " + root);
|
||||
}
|
||||
|
||||
KMap<String, String> result = new KMap<>();
|
||||
for (Map.Entry<String, String> entry : Objects.requireNonNull(configuredWorlds, "configuredWorlds").entrySet()) {
|
||||
String configuredWorldName = entry.getKey();
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromConfiguredWorldName(
|
||||
configuredWorldName,
|
||||
levelNamePath.toString());
|
||||
if (!IrisWorldStorage.configuredWorldName(worldKey, levelNamePath.toString())
|
||||
.equals(configuredWorldName)) {
|
||||
continue;
|
||||
}
|
||||
Path dimensionRoot = IrisWorldStorage.dimensionRoot(root.toFile(), worldKey)
|
||||
.toPath()
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (Files.isDirectory(dimensionRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||
result.put(configuredWorldName, entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static KMap<String, String> readBukkitWorlds(Path levelRoot) {
|
||||
YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
ConfigurationSection worlds = bukkit.getConfigurationSection("worlds");
|
||||
if (worlds == null) return new KMap<>();
|
||||
@@ -212,11 +267,16 @@ public class IrisWorlds {
|
||||
result.put(world, loadKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
return filterBukkitWorldsByStorage(levelRoot, result);
|
||||
}
|
||||
|
||||
private static IrisDimension loadDimension(String worldIdentity, String id) {
|
||||
File pack = IrisWorldStorage.packRoot(WorldIdentity.parse(worldIdentity));
|
||||
private File packRoot(String worldIdentity) {
|
||||
NamespacedKey worldKey = WorldIdentity.parse(worldIdentity);
|
||||
return new File(IrisWorldStorage.dimensionRoot(levelRoot.toFile(), worldKey), "iris/pack");
|
||||
}
|
||||
|
||||
private IrisDimension loadDimension(String worldIdentity, String id) {
|
||||
File pack = packRoot(worldIdentity);
|
||||
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||
if (dimension == null) {
|
||||
dimension = IrisData.loadAnyDimension(id, null);
|
||||
|
||||
@@ -24,6 +24,8 @@ import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService.ReapplyOutcome;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
@@ -276,10 +278,12 @@ public class ServerConfigurator {
|
||||
IrisLogging.debug("Checking Data Packs...");
|
||||
}
|
||||
List<File> packRoots;
|
||||
List<IrisGeneratorBinding> bindings;
|
||||
try {
|
||||
packRoots = collectCompilerPackRoots();
|
||||
bindings = collectConfiguredLevelStemBindings();
|
||||
} catch (IOException exception) {
|
||||
IrisLogging.reportError("Unable to resolve Iris datapack compiler roots.", exception);
|
||||
IrisLogging.reportError("Unable to resolve Iris datapack compiler inputs.", exception);
|
||||
return DatapackInstallResult.failedResult();
|
||||
}
|
||||
|
||||
@@ -303,6 +307,7 @@ public class ServerConfigurator {
|
||||
IrisDatapackCompiler.compile(
|
||||
packRoots,
|
||||
stagedRoots,
|
||||
bindings,
|
||||
fixer,
|
||||
IrisSettings.get().getGeneral().adjustVanillaHeight
|
||||
);
|
||||
@@ -445,11 +450,25 @@ public class ServerConfigurator {
|
||||
IrisWorldStorage.levelRoot().toPath());
|
||||
}
|
||||
|
||||
private static List<IrisGeneratorBinding> collectConfiguredLevelStemBindings() throws IOException {
|
||||
File levelRoot = IrisWorldStorage.levelRoot();
|
||||
String levelId = levelRoot.getName();
|
||||
if (levelId.isBlank()) {
|
||||
throw new IOException("Configured level root has no Paper startup level id: " + levelRoot);
|
||||
}
|
||||
return BukkitWorldConfiguration.readIrisGeneratorBindings(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
levelId,
|
||||
levelRoot.toPath()
|
||||
);
|
||||
}
|
||||
|
||||
private static String computeCurrentDatapackCompilerInputFingerprint(IDataFixer fixer) throws IOException {
|
||||
return IrisDatapackCompiler.computeInputFingerprint(
|
||||
IrisDatapackCompiler.collectCompilerInputRoots(
|
||||
IrisPlatforms.get().dataFolder().toPath(),
|
||||
IrisWorldStorage.levelRoot().toPath()),
|
||||
collectConfiguredLevelStemBindings(),
|
||||
Objects.requireNonNull(fixer, "Datapack fixer"),
|
||||
IrisSettings.get().getGeneral().adjustVanillaHeight);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
@@ -15,6 +18,10 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -91,6 +98,95 @@ public final class BukkitWorldConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<IrisGeneratorBinding> readIrisGeneratorBindings(
|
||||
File configurationFile,
|
||||
String levelName,
|
||||
Path levelRoot
|
||||
) throws IOException {
|
||||
File requiredConfigurationFile = Objects.requireNonNull(configurationFile, "configurationFile");
|
||||
String requiredLevelName = requireName(levelName, "Level name");
|
||||
Path requiredLevelRoot = Objects.requireNonNull(levelRoot, "levelRoot")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
Path configurationPath = requiredConfigurationFile.toPath();
|
||||
if (!Files.exists(configurationPath, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return List.of();
|
||||
}
|
||||
synchronized (MUTATION_LOCK) {
|
||||
YamlConfiguration configuration = load(requiredConfigurationFile);
|
||||
Object rawWorlds = configuration.get("worlds");
|
||||
ConfigurationSection worlds = configuration.getConfigurationSection("worlds");
|
||||
if (rawWorlds != null && worlds == null) {
|
||||
throw new IOException("bukkit.yml worlds entry is not a section.");
|
||||
}
|
||||
if (worlds == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> configuredNames = new ArrayList<>(worlds.getKeys(false));
|
||||
configuredNames.sort(Comparator.naturalOrder());
|
||||
Map<WorldSlotKey, IrisGeneratorBinding> bindings = new LinkedHashMap<>();
|
||||
for (String configuredName : configuredNames) {
|
||||
Object rawWorld = worlds.get(configuredName);
|
||||
ConfigurationSection world = worlds.getConfigurationSection(configuredName);
|
||||
if (rawWorld != null && world == null) {
|
||||
throw new IOException("bukkit.yml world entry \"" + configuredName + "\" is not a section.");
|
||||
}
|
||||
if (world == null || !world.getKeys(false).contains("generator")) {
|
||||
continue;
|
||||
}
|
||||
Object rawGenerator = world.get("generator");
|
||||
if (!(rawGenerator instanceof String generator)) {
|
||||
throw new IOException("bukkit.yml generator for world \"" + configuredName
|
||||
+ "\" is not a string.");
|
||||
}
|
||||
String configuredGenerator = generator.trim();
|
||||
if (!configuredGenerator.equalsIgnoreCase("Iris")
|
||||
&& !configuredGenerator.regionMatches(true, 0, "Iris:", 0, 5)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NamespacedKey namespacedKey;
|
||||
try {
|
||||
namespacedKey = IrisWorldStorage.keyFromConfiguredWorldName(
|
||||
configuredName,
|
||||
requiredLevelName
|
||||
);
|
||||
} catch (IllegalArgumentException failure) {
|
||||
throw new IOException("bukkit.yml contains an invalid Iris world name \""
|
||||
+ configuredName + "\".", failure);
|
||||
}
|
||||
if (!"iris".equals(namespacedKey.getNamespace())) {
|
||||
continue;
|
||||
}
|
||||
if (!configuredName.equals(IrisWorldStorage.configuredWorldName(
|
||||
namespacedKey,
|
||||
requiredLevelName
|
||||
))) {
|
||||
continue;
|
||||
}
|
||||
if (!IrisWorldStorage.isExistingManagedDimensionRoot(
|
||||
requiredLevelRoot.toFile(),
|
||||
namespacedKey
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
String dimension = selectedIrisDimension(configuredGenerator, configuredName);
|
||||
WorldSlotKey worldKey = new WorldSlotKey(
|
||||
namespacedKey.getNamespace(),
|
||||
namespacedKey.getKey()
|
||||
);
|
||||
IrisGeneratorBinding binding = new IrisGeneratorBinding(configuredName, worldKey, dimension);
|
||||
IrisGeneratorBinding previous = bindings.putIfAbsent(worldKey, binding);
|
||||
if (previous != null) {
|
||||
throw new IOException("bukkit.yml maps both \"" + previous.configuredWorldName()
|
||||
+ "\" and \"" + configuredName + "\" to " + worldKey + ".");
|
||||
}
|
||||
}
|
||||
return List.copyOf(bindings.values());
|
||||
}
|
||||
}
|
||||
|
||||
public static GeneratorReplacement replaceIfMatching(
|
||||
File configurationFile,
|
||||
String worldName,
|
||||
@@ -396,11 +492,40 @@ public final class BukkitWorldConfiguration {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String selectedIrisDimension(String configuredGenerator, String worldName) throws IOException {
|
||||
if (configuredGenerator.equalsIgnoreCase("Iris")) {
|
||||
throw new IOException("bukkit.yml Iris generator for custom world \"" + worldName
|
||||
+ "\" must select a dimension with Iris:<dimension>.");
|
||||
}
|
||||
String dimension = configuredGenerator.substring(5).trim();
|
||||
if (dimension.isEmpty()) {
|
||||
throw new IOException("bukkit.yml Iris generator for world \"" + worldName
|
||||
+ "\" does not select a dimension.");
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
public enum Registration {
|
||||
CREATED,
|
||||
UNCHANGED
|
||||
}
|
||||
|
||||
public record IrisGeneratorBinding(
|
||||
String configuredWorldName,
|
||||
WorldSlotKey worldKey,
|
||||
String dimension
|
||||
) {
|
||||
public IrisGeneratorBinding {
|
||||
configuredWorldName = requireName(configuredWorldName, "Configured world name");
|
||||
WorldSlotKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey");
|
||||
if (!"iris".equals(requiredWorldKey.namespace())
|
||||
|| !requiredWorldKey.key().matches("[a-z0-9_-]+")) {
|
||||
throw new IllegalArgumentException("Only safe Iris-managed keys can have LevelStem bindings.");
|
||||
}
|
||||
dimension = requireName(dimension, "Iris dimension");
|
||||
}
|
||||
}
|
||||
|
||||
public record WorldGeneratorSnapshot(
|
||||
boolean worldsSectionPresent,
|
||||
boolean worldSectionPresent,
|
||||
|
||||
@@ -18,6 +18,7 @@ import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@@ -695,6 +696,28 @@ public final class IrisWorldRemovalService {
|
||||
}
|
||||
}
|
||||
|
||||
static String bukkitConfigurationWorldName(WorldRemovalPathPolicy.Target target) {
|
||||
WorldRemovalPathPolicy.Target requiredTarget = Objects.requireNonNull(target, "target");
|
||||
Path levelName = requiredTarget.levelRoot().getFileName();
|
||||
if (levelName == null || levelName.toString().isBlank()) {
|
||||
throw new IllegalArgumentException("Level root must have a Bukkit startup name.");
|
||||
}
|
||||
return IrisWorldStorage.configuredWorldName(requiredTarget.worldKey(), levelName.toString());
|
||||
}
|
||||
|
||||
static String bukkitGenerator(
|
||||
YamlConfiguration configuration,
|
||||
WorldRemovalPathPolicy.Target target
|
||||
) {
|
||||
ConfigurationSection worlds = Objects.requireNonNull(configuration, "configuration")
|
||||
.getConfigurationSection("worlds");
|
||||
if (worlds == null) {
|
||||
return null;
|
||||
}
|
||||
ConfigurationSection world = worlds.getConfigurationSection(bukkitConfigurationWorldName(target));
|
||||
return world == null ? null : world.getString("generator");
|
||||
}
|
||||
|
||||
private static final class RemovalFailure extends CompletionException {
|
||||
private final RemovalStatus status;
|
||||
private final Path quarantineDirectory;
|
||||
@@ -840,7 +863,7 @@ public final class IrisWorldRemovalService {
|
||||
try {
|
||||
boolean bukkitChanged = BukkitWorldConfiguration.remove(
|
||||
ServerProperties.BUKKIT_YML,
|
||||
target.logicalName()
|
||||
bukkitConfigurationWorldName(target)
|
||||
);
|
||||
return new ConfigurationDisposition(multiverseChanged || bukkitChanged, null);
|
||||
} catch (Throwable failure) {
|
||||
@@ -931,7 +954,7 @@ public final class IrisWorldRemovalService {
|
||||
);
|
||||
boolean directoryPresent = Files.isDirectory(target.worldDirectory(), LinkOption.NOFOLLOW_LINKS);
|
||||
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
String generator = configuration.getString("worlds." + target.logicalName() + ".generator");
|
||||
String generator = bukkitGenerator(configuration, target);
|
||||
boolean configurationManaged = generator != null
|
||||
&& (generator.equalsIgnoreCase("Iris") || generator.regionMatches(true, 0, "Iris:", 0, 5));
|
||||
boolean conflictingConfiguration = generator != null && !configurationManaged;
|
||||
|
||||
@@ -180,7 +180,6 @@ public final class LifecycleOperationCoordinator {
|
||||
WORLD_UNLOAD,
|
||||
WORLD_REMOVE,
|
||||
WORLD_REPLACE,
|
||||
WORLD_PROMOTE,
|
||||
STUDIO_OPEN,
|
||||
STUDIO_CLOSE,
|
||||
PACK_CREATE,
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -27,6 +28,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class WorldReplacementFilesystem {
|
||||
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
|
||||
private static final List<Path> PAPER_WORLD_METADATA = List.of(
|
||||
Path.of("data/paper/metadata.dat"),
|
||||
Path.of("data/paper/level_overrides.dat"),
|
||||
@@ -55,15 +57,15 @@ public final class WorldReplacementFilesystem {
|
||||
ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths");
|
||||
State state = inspect(requiredPaths);
|
||||
if (!state.targetPresent()) {
|
||||
throw new IOException("overwrite=true requires an existing exact world slot; use ordinary create for a new world.");
|
||||
throw new IOException("/iris replace requires an existing exact world slot; use /iris create for a new world.");
|
||||
}
|
||||
if (state.stagePresent() || state.backupPresent()) {
|
||||
throw new IOException("A replacement artifact already exists for this transaction.");
|
||||
}
|
||||
requireMigratedRetainedWorld(requiredPaths.target());
|
||||
requireCurrentPaperWorld(requiredPaths.target());
|
||||
}
|
||||
|
||||
public static void requireMigratedRetainedWorld(Path retainedWorld) throws IOException {
|
||||
public static void requireCurrentPaperWorld(Path retainedWorld) throws IOException {
|
||||
try {
|
||||
requireDirectory(retainedWorld, "retained world");
|
||||
requireDirectory(retainedWorld.resolve("data"), "retained world data");
|
||||
@@ -249,13 +251,15 @@ public final class WorldReplacementFilesystem {
|
||||
try (Stream<Path> stream = Files.walk(root)) {
|
||||
files = stream
|
||||
.filter(path -> !path.equals(root))
|
||||
.filter(path -> !containsMetadataSegment(root.relativize(path)))
|
||||
.sorted(Comparator.comparing(path -> root.relativize(path).toString()))
|
||||
.toList();
|
||||
}
|
||||
for (Path file : files) {
|
||||
BasicFileAttributes attributes = requireSafeEntry(file);
|
||||
Path relative = root.relativize(file);
|
||||
if (isGeneratedPackMetadata(relative, attributes)) {
|
||||
continue;
|
||||
}
|
||||
update(digest, relative.toString().replace(file.getFileSystem().getSeparator(), "/"));
|
||||
digest.update((byte) (attributes.isDirectory() ? 1 : 0));
|
||||
if (!attributes.isRegularFile()) {
|
||||
@@ -304,13 +308,10 @@ public final class WorldReplacementFilesystem {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
private static boolean containsMetadataSegment(Path relative) {
|
||||
for (Path component : relative) {
|
||||
if (".iris".equals(component.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
private static boolean isGeneratedPackMetadata(Path relative, BasicFileAttributes attributes) {
|
||||
return PackDirectoryResolver.isHiddenName(relative.getName(0).toString())
|
||||
|| attributes.isRegularFile()
|
||||
&& relative.getFileName().toString().endsWith(CODE_WORKSPACE_SUFFIX);
|
||||
}
|
||||
|
||||
private static BasicFileAttributes requireSafeEntry(Path path) throws IOException {
|
||||
@@ -347,7 +348,7 @@ public final class WorldReplacementFilesystem {
|
||||
}
|
||||
|
||||
private static void preservePaperWorldMetadata(Path retainedWorld, Path replacementWorld) throws IOException {
|
||||
requireMigratedRetainedWorld(retainedWorld);
|
||||
requireCurrentPaperWorld(retainedWorld);
|
||||
requireDirectory(replacementWorld, "replacement world");
|
||||
ensureDirectory(replacementWorld.resolve("data"), "replacement world data");
|
||||
ensureDirectory(replacementWorld.resolve("data/paper"), "replacement Paper data");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot;
|
||||
|
||||
@@ -152,15 +153,7 @@ public final class WorldReplacementJournal {
|
||||
}
|
||||
String levelName = fileName.toString();
|
||||
if ("iris".equals(requiredWorldKey.namespace())) {
|
||||
String logicalName = requiredWorldKey.key();
|
||||
if (logicalName.equals(levelName)
|
||||
|| logicalName.equals(levelName + "_nether")
|
||||
|| logicalName.equals(levelName + "_the_end")) {
|
||||
throw new IllegalArgumentException(
|
||||
"An Iris-managed world cannot use a configured vanilla world alias."
|
||||
);
|
||||
}
|
||||
return logicalName;
|
||||
return IrisWorldStorage.configuredWorldName(requiredWorldKey, levelName);
|
||||
}
|
||||
if (WorldSlotKey.minecraft("overworld").equals(requiredWorldKey)) {
|
||||
return levelName;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.volmlib.util.nbt.io.NBTUtil;
|
||||
import art.arcane.volmlib.util.nbt.io.NamedTag;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import art.arcane.volmlib.util.nbt.tag.LongTag;
|
||||
import art.arcane.volmlib.util.nbt.tag.Tag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class WorldReplacementSeed {
|
||||
private static final Path WORLD_GEN_SETTINGS = Path.of("data/minecraft/world_gen_settings.dat");
|
||||
|
||||
private WorldReplacementSeed() {
|
||||
}
|
||||
|
||||
public static long readAuthoritativeSeed(Path worldDirectory) throws IOException {
|
||||
Path requiredWorldDirectory = Objects.requireNonNull(worldDirectory, "worldDirectory")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
Path settings = requiredWorldDirectory.resolve(WORLD_GEN_SETTINGS);
|
||||
NamedTag namedTag = readSettings(settings);
|
||||
return requireData(namedTag, settings).getLongTag("seed").asLong();
|
||||
}
|
||||
|
||||
public static void copyWithAuthoritativeSeed(
|
||||
Path sourceWorldDirectory,
|
||||
Path targetWorldDirectory,
|
||||
long seed
|
||||
) throws IOException {
|
||||
Path sourceWorld = Objects.requireNonNull(sourceWorldDirectory, "sourceWorldDirectory")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
Path targetWorld = Objects.requireNonNull(targetWorldDirectory, "targetWorldDirectory")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
Path source = sourceWorld.resolve(WORLD_GEN_SETTINGS);
|
||||
Path target = targetWorld.resolve(WORLD_GEN_SETTINGS);
|
||||
NamedTag namedTag = readSettings(source);
|
||||
CompoundTag data = requireData(namedTag, source);
|
||||
data.putLong("seed", seed);
|
||||
|
||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) {
|
||||
throw new IOException("Staged Paper world generation settings already exist: " + target);
|
||||
}
|
||||
Path parent = target.getParent();
|
||||
if (parent == null) {
|
||||
throw new IOException("Staged Paper world generation settings have no parent: " + target);
|
||||
}
|
||||
Files.createDirectories(parent);
|
||||
Path staged = Files.createTempFile(parent, ".world-gen-settings-", ".dat");
|
||||
try {
|
||||
NBTUtil.write(namedTag, staged.toFile(), true);
|
||||
try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
}
|
||||
try {
|
||||
Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(staged, target);
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(staged);
|
||||
}
|
||||
|
||||
long writtenSeed = readAuthoritativeSeed(targetWorld);
|
||||
if (writtenSeed != seed) {
|
||||
throw new IOException("Staged Paper world generation settings did not retain the requested seed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static NamedTag readSettings(Path settings) throws IOException {
|
||||
BasicFileAttributes attributes = Files.readAttributes(
|
||||
settings,
|
||||
BasicFileAttributes.class,
|
||||
LinkOption.NOFOLLOW_LINKS
|
||||
);
|
||||
if (attributes.isSymbolicLink() || !attributes.isRegularFile()) {
|
||||
throw new IOException("Paper world generation settings are not a regular file: " + settings);
|
||||
}
|
||||
|
||||
try {
|
||||
return NBTUtil.read(settings.toFile());
|
||||
} catch (IOException failure) {
|
||||
throw new IOException("Could not read Paper world generation settings: " + settings, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompoundTag requireData(NamedTag namedTag, Path settings) throws IOException {
|
||||
Tag<?> rootTag = namedTag.getTag();
|
||||
if (!(rootTag instanceof CompoundTag root)) {
|
||||
throw new IOException("Paper world generation settings must have a compound root: " + settings);
|
||||
}
|
||||
Tag<?> dataTag = root.get("data");
|
||||
if (dataTag == null) {
|
||||
throw new IOException("Paper world generation settings are missing data: " + settings);
|
||||
}
|
||||
if (!(dataTag instanceof CompoundTag data)) {
|
||||
throw new IOException("Paper world generation settings data must be a compound tag: " + settings);
|
||||
}
|
||||
Tag<?> seedTag = data.get("seed");
|
||||
if (seedTag == null) {
|
||||
throw new IOException("Paper world generation settings are missing data.seed: " + settings);
|
||||
}
|
||||
if (!(seedTag instanceof LongTag seed)) {
|
||||
throw new IOException("Paper world generation settings data.seed must be a long tag: " + settings);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
+4
-24
@@ -175,9 +175,9 @@ public final class BukkitCommandMessagesExtended {
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend",
|
||||
C.YELLOW + "Try one of: overworld, vanilla, flat, theend"
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTART_SERVER_GENERATE_LOAD = TextKey.of(
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load",
|
||||
C.GREEN + "World staging completed. Restart the server to generate/load \"" + "{worldName}" + "\"."
|
||||
public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD = TextKey.of(
|
||||
"iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load",
|
||||
C.GREEN + "World staging completed. Iris is restarting the server to generate/load \"" + "{worldName}" + "\"."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS = TextKey.of(
|
||||
"iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details",
|
||||
@@ -187,10 +187,6 @@ public final class BukkitCommandMessagesExtended {
|
||||
"iris.bukkit.commandiris.successfully_created_your_world",
|
||||
C.GREEN + "Successfully created your world!"
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_YOUR_WORLD_WILL_AUTOMATICALLY_BE_SET_AS_MAIN_WORLD_WHEN = TextKey.of(
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when",
|
||||
C.GREEN + "Your world will automatically be set as the main world when the server restarts."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA = TextKey.of(
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia",
|
||||
C.YELLOW + "Runtime world creation is disabled on Folia."
|
||||
@@ -203,22 +199,10 @@ public final class BukkitCommandMessagesExtended {
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension",
|
||||
C.RED + "Failed to stage world files for dimension \"" + "{value}" + "\"."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_UPDATED_SERVER_PROPERTIES_LEVEL_NAME = TextKey.of(
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name",
|
||||
C.GREEN + "Updated server.properties level-name to \"" + "{name}" + "\"."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_WORLD_WAS_STAGED_BUT_FAILED_UPDATE_SERVER_PROPERTIES_MAIN_WORLD = TextKey.of(
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world",
|
||||
C.RED + "World was staged, but failed to update server.properties main world."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED = TextKey.of(
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed",
|
||||
C.GREEN + "Staged Iris world \"" + "{name}" + "\" with generator Iris:" + "{value}" + " and seed " + "{seed}" + "."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_THIS_WORLD_IS_NOW_CONFIGURED_AS_MAIN_NEXT_RESTART = TextKey.of(
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart",
|
||||
C.GREEN + "This world is now configured as main for next restart."
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML = TextKey.of(
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml",
|
||||
C.RED + "Failed to update bukkit.yml: " + "{value}"
|
||||
@@ -878,17 +862,13 @@ public final class BukkitCommandMessagesExtended {
|
||||
COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2,
|
||||
COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS,
|
||||
COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND,
|
||||
COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTART_SERVER_GENERATE_LOAD,
|
||||
COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD,
|
||||
COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS,
|
||||
COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD,
|
||||
COMMAND_IRIS_YOUR_WORLD_WILL_AUTOMATICALLY_BE_SET_AS_MAIN_WORLD_WHEN,
|
||||
COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA,
|
||||
COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP,
|
||||
COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION,
|
||||
COMMAND_IRIS_UPDATED_SERVER_PROPERTIES_LEVEL_NAME,
|
||||
COMMAND_IRIS_WORLD_WAS_STAGED_BUT_FAILED_UPDATE_SERVER_PROPERTIES_MAIN_WORLD,
|
||||
COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED,
|
||||
COMMAND_IRIS_THIS_WORLD_IS_NOW_CONFIGURED_AS_MAIN_NEXT_RESTART,
|
||||
COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML,
|
||||
COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST,
|
||||
COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE,
|
||||
|
||||
@@ -282,10 +282,6 @@ public final class DirectorCommandMessages {
|
||||
"iris.director.commandiris.param.seed_generate_world_with",
|
||||
"The seed to generate the world with"
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_PARAM_WHETHER_NOT_AUTOMATICALLY_USE_THIS_WORLD_AS_MAIN_WORLD = TextKey.of(
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world",
|
||||
"Whether or not to automatically use this world as the main world"
|
||||
);
|
||||
public static final TextKey COMMAND_IRIS_PARAM_REPLACE_EXACT_EXISTING_WORLD_SLOT_NEXT_RESTART = TextKey.of(
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
|
||||
"Replace the exact existing world slot on the next restart"
|
||||
@@ -941,7 +937,6 @@ public final class DirectorCommandMessages {
|
||||
COMMAND_IRIS_PARAM_NAME_WORLD_CREATE,
|
||||
COMMAND_IRIS_PARAM_DIMENSION_PACK_CREATE_WORLD_WITH,
|
||||
COMMAND_IRIS_PARAM_SEED_GENERATE_WORLD_WITH,
|
||||
COMMAND_IRIS_PARAM_WHETHER_NOT_AUTOMATICALLY_USE_THIS_WORLD_AS_MAIN_WORLD,
|
||||
COMMAND_IRIS_PARAM_REPLACE_EXACT_EXISTING_WORLD_SLOT_NEXT_RESTART,
|
||||
COMMAND_IRIS_DIRECTOR_TELEPORT_ANOTHER_WORLD,
|
||||
COMMAND_IRIS_PARAM_WORLD_TELEPORT,
|
||||
|
||||
@@ -54,6 +54,8 @@ import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -269,6 +271,14 @@ public interface INMSBinding {
|
||||
default void uninjectBukkit() {
|
||||
}
|
||||
|
||||
default void writeCurrentPaperWorldData(
|
||||
Path sourceWorldDirectory,
|
||||
Path targetWorldDirectory,
|
||||
long seed
|
||||
) throws IOException {
|
||||
throw new UnsupportedOperationException("The active NMS binding does not support current Paper world data staging.");
|
||||
}
|
||||
|
||||
KMap<Material, List<BlockProperty>> getBlockProperties();
|
||||
|
||||
private void validateDimensionTypes(WorldCreator c) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.pack;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
@@ -44,6 +45,22 @@ public final class AtomicDirectoryPublisher {
|
||||
}
|
||||
}
|
||||
|
||||
public static Publication publishAbsent(Path stagedDirectory, Path targetDirectory) throws IOException {
|
||||
Path staged = Objects.requireNonNull(stagedDirectory, "stagedDirectory").toAbsolutePath().normalize();
|
||||
Path target = Objects.requireNonNull(targetDirectory, "targetDirectory").toAbsolutePath().normalize();
|
||||
if (!Files.isDirectory(staged) || Files.isSymbolicLink(staged)) {
|
||||
throw new IOException("Staged directory is missing or unsafe: " + staged);
|
||||
}
|
||||
if (!Objects.equals(staged.getParent(), target.getParent())) {
|
||||
throw new IOException("Staged and target directories must have the same parent.");
|
||||
}
|
||||
if (Files.exists(target) || Files.isSymbolicLink(target)) {
|
||||
throw new FileAlreadyExistsException("Publication target already exists: " + target);
|
||||
}
|
||||
move(staged, target);
|
||||
return new Publication(target, null);
|
||||
}
|
||||
|
||||
private static void move(Path source, Path target) throws IOException {
|
||||
try {
|
||||
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
|
||||
|
||||
@@ -2,6 +2,8 @@ package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.core.IrisDatapackCompiler;
|
||||
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -45,7 +47,7 @@ import java.util.zip.ZipInputStream;
|
||||
public final class DefaultPackBootstrapProvisioner {
|
||||
private static final List<PackSpec> DEFAULT_PACKS = List.of();
|
||||
private static final String WORLD_DATAPACK_DIRECTORY = "iris";
|
||||
private static final int MARKER_SCHEMA = 5;
|
||||
private static final int MARKER_SCHEMA = 6;
|
||||
private static final int MAX_ARCHIVE_ENTRIES = 100_000;
|
||||
private static final long MAX_ARCHIVE_BYTES = 512L * 1024L * 1024L;
|
||||
private static final long MAX_EXPANDED_BYTES = 2L * 1024L * 1024L * 1024L;
|
||||
@@ -71,13 +73,20 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(dataDirectory, "dataDirectory");
|
||||
Objects.requireNonNull(feedback, "feedback");
|
||||
Path levelRoot = Objects.requireNonNull(startupPaths, "startupPaths").levelRoot();
|
||||
BukkitStartupPaths requiredStartupPaths = Objects.requireNonNull(startupPaths, "startupPaths");
|
||||
Path levelRoot = requiredStartupPaths.levelRoot();
|
||||
List<IrisGeneratorBinding> bindings = BukkitWorldConfiguration.readIrisGeneratorBindings(
|
||||
requiredStartupPaths.bukkitConfiguration().toFile(),
|
||||
levelId(levelRoot),
|
||||
requiredStartupPaths.levelRoot()
|
||||
);
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(15))
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.build();
|
||||
ProvisionOptions options = new ProvisionOptions(
|
||||
DEFAULT_PACKS,
|
||||
bindings,
|
||||
client,
|
||||
Clock.systemUTC(),
|
||||
Duration.ofMinutes(30),
|
||||
@@ -95,20 +104,24 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return isProvisioned(
|
||||
dataDirectory,
|
||||
BukkitStartupPaths.resolveCurrent().levelRoot()
|
||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
|
||||
List<IrisGeneratorBinding> bindings = BukkitWorldConfiguration.readIrisGeneratorBindings(
|
||||
startupPaths.bukkitConfiguration().toFile(),
|
||||
levelId(startupPaths.levelRoot()),
|
||||
startupPaths.levelRoot()
|
||||
);
|
||||
return isProvisioned(dataDirectory, startupPaths.levelRoot(), DEFAULT_PACKS, bindings);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isProvisioned(Path dataDirectory, Path levelRoot) {
|
||||
return isProvisioned(dataDirectory, levelRoot, DEFAULT_PACKS);
|
||||
}
|
||||
|
||||
static boolean isProvisioned(Path dataDirectory, Path levelRoot, List<PackSpec> requiredPacks) {
|
||||
static boolean isProvisioned(
|
||||
Path dataDirectory,
|
||||
Path levelRoot,
|
||||
List<PackSpec> requiredPacks,
|
||||
List<IrisGeneratorBinding> bindings
|
||||
) {
|
||||
try {
|
||||
Path normalizedData = dataDirectory.toAbsolutePath().normalize();
|
||||
Path normalizedLevel = levelRoot.toAbsolutePath().normalize();
|
||||
@@ -137,12 +150,19 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return directoryFingerprint(datapackRoot).equals(marker.getProperty("datapackFingerprint"))
|
||||
&& datapackRoot.toString().equals(marker.getProperty("datapackPath"))
|
||||
&& packRootsFingerprint(IrisDatapackCompiler.collectPackRoots(
|
||||
List<File> packRoots = IrisDatapackCompiler.collectPackRoots(
|
||||
normalizedData,
|
||||
normalizedLevel
|
||||
)).equals(marker.getProperty("aggregateFingerprint"));
|
||||
);
|
||||
return directoryFingerprint(datapackRoot).equals(marker.getProperty("datapackFingerprint"))
|
||||
&& datapackRoot.toString().equals(marker.getProperty("datapackPath"))
|
||||
&& packRootsFingerprint(packRoots).equals(marker.getProperty("aggregateFingerprint"))
|
||||
&& IrisDatapackCompiler.computeInputFingerprint(
|
||||
packRoots,
|
||||
bindings,
|
||||
fixer,
|
||||
false
|
||||
).equals(marker.getProperty("compilerInputFingerprint"));
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
return false;
|
||||
}
|
||||
@@ -234,10 +254,17 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
}
|
||||
String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer);
|
||||
String aggregateFingerprint = packRootsFingerprint(packRoots);
|
||||
String compilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||
packRoots,
|
||||
options.bindings(),
|
||||
fixer,
|
||||
false
|
||||
);
|
||||
boolean anyPackReplaced = !packPublications.isEmpty();
|
||||
boolean rebuildDatapack = anyPackReplaced
|
||||
|| !existingDatapack
|
||||
|| !aggregateFingerprint.equals(previousMarker.getProperty("aggregateFingerprint"))
|
||||
|| !compilerInputFingerprint.equals(previousMarker.getProperty("compilerInputFingerprint"))
|
||||
|| !compilerIdentity.equals(previousMarker.getProperty("compilerIdentity"))
|
||||
|| !datapackRoot.toString().equals(previousMarker.getProperty("datapackPath"))
|
||||
|| !directoryFingerprint(datapackRoot).equals(previousMarker.getProperty("datapackFingerprint"));
|
||||
@@ -245,7 +272,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
compileContainer = datapacksRoot.resolve("." + WORLD_DATAPACK_DIRECTORY + "-stage-" + UUID.randomUUID());
|
||||
Files.createDirectories(compileContainer);
|
||||
KList<File> outputFolders = new KList<File>().qadd(compileContainer.toFile());
|
||||
IrisDatapackCompiler.compile(packRoots, outputFolders, fixer, false);
|
||||
IrisDatapackCompiler.compile(packRoots, outputFolders, options.bindings(), fixer, false);
|
||||
if (!isDatapackRoot(compileContainer, !packRoots.isEmpty())) {
|
||||
throw new IOException("Canonical Iris datapack compiler produced incomplete output at " + compileContainer);
|
||||
}
|
||||
@@ -266,6 +293,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
String finalAggregateFingerprint = packRootsFingerprint(finalPackRoots);
|
||||
String finalCompilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||
finalPackRoots,
|
||||
options.bindings(),
|
||||
fixer,
|
||||
false);
|
||||
String finalDatapackFingerprint = directoryFingerprint(datapackRoot);
|
||||
@@ -280,6 +308,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
}
|
||||
marker.setProperty("aggregateFingerprint", finalAggregateFingerprint);
|
||||
marker.setProperty("compilerIdentity", compilerIdentity);
|
||||
marker.setProperty("compilerInputFingerprint", finalCompilerInputFingerprint);
|
||||
marker.setProperty("datapackFingerprint", finalDatapackFingerprint);
|
||||
marker.setProperty("datapackPath", datapackRoot.toString());
|
||||
marker.setProperty("completedAt", Long.toString(options.clock().millis()));
|
||||
@@ -765,6 +794,14 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
}
|
||||
}
|
||||
|
||||
private static String levelId(Path levelRoot) throws IOException {
|
||||
Path fileName = Objects.requireNonNull(levelRoot, "levelRoot").getFileName();
|
||||
if (fileName == null || fileName.toString().isBlank()) {
|
||||
throw new IOException("Configured level root has no Paper startup level id: " + levelRoot);
|
||||
}
|
||||
return fileName.toString();
|
||||
}
|
||||
|
||||
private static void deleteQuietly(Path path, Consumer<String> feedback) {
|
||||
try {
|
||||
delete(path);
|
||||
@@ -880,6 +917,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
|
||||
record ProvisionOptions(
|
||||
List<PackSpec> packs,
|
||||
List<IrisGeneratorBinding> bindings,
|
||||
HttpClient client,
|
||||
Clock clock,
|
||||
Duration refreshInterval,
|
||||
@@ -891,6 +929,7 @@ public final class DefaultPackBootstrapProvisioner {
|
||||
) {
|
||||
ProvisionOptions {
|
||||
packs = List.copyOf(Objects.requireNonNull(packs, "packs"));
|
||||
bindings = List.copyOf(Objects.requireNonNull(bindings, "bindings"));
|
||||
Objects.requireNonNull(client, "client");
|
||||
Objects.requireNonNull(clock, "clock");
|
||||
Objects.requireNonNull(refreshInterval, "refreshInterval");
|
||||
|
||||
@@ -134,8 +134,11 @@ public class IrisCreator {
|
||||
private BiConsumer<String, Long> studioTimingConsumer;
|
||||
private DatapackPreparation datapackPreparation = DatapackPreparation.INSTALL_IF_CHANGED;
|
||||
|
||||
public static boolean removeFromBukkitYml(String name) throws IOException {
|
||||
return BukkitWorldConfiguration.remove(BUKKIT_YML, name);
|
||||
public static boolean removeFromBukkitYml(NamespacedKey worldKey) throws IOException {
|
||||
return BukkitWorldConfiguration.remove(
|
||||
BUKKIT_YML,
|
||||
IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName())
|
||||
);
|
||||
}
|
||||
|
||||
public static int removeTransientStudioWorldsFromBukkitYml() throws IOException {
|
||||
@@ -293,7 +296,12 @@ public class IrisCreator {
|
||||
reportStudioProgress(0.86D, "create_world");
|
||||
|
||||
if (!studio && !benchmark) {
|
||||
BukkitWorldConfiguration.register(BUKKIT_YML, name, dimension, seed);
|
||||
BukkitWorldConfiguration.register(
|
||||
BUKKIT_YML,
|
||||
IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName()),
|
||||
dimension,
|
||||
seed
|
||||
);
|
||||
bukkitRegistered = true;
|
||||
World createdWorld = world;
|
||||
CompletableFuture<Void> multiverseRegistration = J.sfut(
|
||||
@@ -730,7 +738,10 @@ public class IrisCreator {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
try {
|
||||
BukkitWorldConfiguration.remove(BUKKIT_YML, name);
|
||||
BukkitWorldConfiguration.remove(
|
||||
BUKKIT_YML,
|
||||
IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName())
|
||||
);
|
||||
} catch (Throwable rollbackFailure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
|
||||
@@ -670,7 +670,7 @@ public class IrisToolbelt {
|
||||
}
|
||||
|
||||
public static boolean removeWorld(World world) throws IOException {
|
||||
return IrisCreator.removeFromBukkitYml(IrisWorldStorage.logicalName(world));
|
||||
return IrisCreator.removeFromBukkitYml(WorldIdentity.key(world));
|
||||
}
|
||||
|
||||
record PackReference(String pack, String dimension, boolean explicitDimension) {
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cWie wäre es stattdessen mit dem Namen \"IrisWorld\"?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cDieser Ordner existiert bereits!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eVersuche eine der folgenden Optionen: overworld, vanilla, flat, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aVorbereitung der Welt abgeschlossen. Starte den Server 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.successfully_created_your_world": "§aDeine Welt wurde erfolgreich erstellt!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aIhre Welt wird automatisch als Hauptwelt festgelegt, wenn der Server neu gestartet wird.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eDie Erstellung von Welten zur Laufzeit ist unter Folia deaktiviert.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eWeltdateien und bukkit.yml werden für den nächsten Start vorbereitet...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cDie Weltdateien für Dimension \"{value}\" konnten nicht vorbereitet werden.",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aDie Eigenschaft level-name in server.properties wurde auf \"{name}\" gesetzt.",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cDie Welt wurde vorbereitet, aber die Hauptwelt in server.properties konnte nicht aktualisiert werden.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aIris-Welt \"{name}\" mit Generator Iris:{value} und Seed {seed} vorbereitet.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aDiese Welt ist für den nächsten Neustart als Hauptwelt konfiguriert.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cbukkit.yml konnte nicht aktualisiert werden: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cDer angegebene Spieler existiert nicht.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} von Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Name der zu erstellenden Welt",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Dimension oder Pack, mit der bzw. dem die Welt erstellt wird",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Seed für die Generierung der Welt",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Ob man diese Welt automatisch als Hauptwelt benutzt oder nicht",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Den exakten vorhandenen Welt-Slot beim nächsten Neustart ersetzen",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teleportieren in eine andere Welt",
|
||||
"iris.director.commandiris.param.world_teleport": "Zielwelt der Teleportation",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c¿Podemos sugerir el nombre \"IrisWorld\"?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c¡Esa carpeta ya existe!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePrueba una de estas opciones: overworld, vanilla, flat, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aPreparación del mundo completada. Reinicia 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.successfully_created_your_world": "§a¡Tu mundo se creó correctamente!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aTu mundo se establecerá automáticamente como mundo principal cuando se reinicie el servidor.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eLa creación de mundos en tiempo de ejecución está desactivada en Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§ePreparando los archivos del mundo y bukkit.yml para el próximo inicio...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cNo se pudieron preparar los archivos del mundo para la dimensión \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aSe actualizó level-name en server.properties a \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cEl mundo se preparó, pero no se pudo actualizar el mundo principal en server.properties.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aSe preparó el mundo de Iris \"{name}\" con el generador Iris:{value} y la semilla {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aEste mundo está configurado como principal para el próximo reinicio.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cNo se pudo actualizar bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cEl jugador especificado no existe.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} por Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "El nombre del mundo que se creará",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "La dimensión o el pack con el que se creará el mundo",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "La semilla con la que se generará el mundo",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Indica si este mundo debe usarse automáticamente como mundo principal",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Reemplazar el espacio exacto del mundo existente en el próximo reinicio",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teletransportarse a otro mundo",
|
||||
"iris.director.commandiris.param.world_teleport": "El mundo al que se teletransportará",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cVoimmeko ehdottaa nimeä \"IrisWorldSen sijaan?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cSe kansio on jo olemassa!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eKokeile yksi: maapallo, vanilla, litteä, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aMaailman valmistelu valmis. Käynnistä palvelin 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.successfully_created_your_world": "§aOnnistuneesti loin maailmasi!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aMaailmasi asetetaan automaattisesti päämaailmaksi, kun palvelin käynnistyy uudelleen.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eRuntime maailman luominen on pois käytöstä Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eValmistellaan maailman tiedostoja ja bukkit.yml seuraavan käynnistyksen...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cMaailmatiedostoja ei voitu lavastaa ulottuvuudeksi \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aPäivitetty server.properties tason nimi \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cMaailma lavastettiin, mutta sitä ei päivitetty server.properties suurmaailma.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aIris-maailma \"{name}\" valmisteltiin generaattorilla Iris:{value} ja siemenellä {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aTämä maailma on nyt asetettu pääohjelmaksi seuraavaan uudelleenkäynnistykseen.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cPäivitys epäonnistui bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cMääriteltyä pelaajaa ei ole olemassa.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris{value} mennessä Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Luotavan maailman nimi",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Ulottuvuus / paketti luoda maailma",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Siemenet tuottaa maailman kanssa",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Käytetäänkö tätä maailmaa automaattisesti päämaailmana",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Korvaa täsmällinen olemassa oleva maailmapaikka seuraavalla uudelleenkäynnistyksellä",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teleporttautuminen toiseen maailmaan",
|
||||
"iris.director.commandiris.param.world_teleport": "Maailman teleportata",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cNous vous suggérons plutôt le nom \"IrisWorld\".",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cCe dossier existe déjà !",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eEssayez l'une des options suivantes : overworld, vanilla, flat, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aPréparation du monde terminée. Redémarrez 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.successfully_created_your_world": "§aVotre monde a été créé avec succès !",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aVotre monde deviendra automatiquement le monde principal au redémarrage du serveur.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eLa création de mondes pendant l'exécution est désactivée sur Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§ePréparation des fichiers du monde et de bukkit.yml pour le prochain démarrage...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cImpossible de préparer les fichiers du monde pour la dimension \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aLa propriété level-name de server.properties a été définie sur \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cLe monde a été préparé, mais le monde principal n'a pas pu être mis à jour dans server.properties.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aLe monde Iris \"{name}\" a été préparé avec le générateur Iris:{value} et la graine {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aCe monde est maintenant configuré comme monde principal pour le prochain redémarrage.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cImpossible de mettre à jour bukkit.yml : {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cLe joueur indiqué n'existe pas.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} par Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Le nom du monde à créer",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "La dimension ou le pack avec lequel créer le monde",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "La graine avec laquelle générer le monde",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Indique si ce monde doit être utilisé automatiquement comme monde principal",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Remplacer l’emplacement exact du monde existant au prochain redémarrage",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Se téléporter vers un autre monde",
|
||||
"iris.director.commandiris.param.world_teleport": "Le monde vers lequel se téléporter",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cאפשר להציע את השם \"IrisWorld\"במקום?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cהתיקיה הזו כבר קיימת!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eנסה: Overworld vanilla, שטוח, TheendXTry",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aהכנת העולם הושלמה. הפעל מחדש את השרת כדי ליצור/לטעון את \"{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.successfully_created_your_world": "§aיצרתם בהצלחה את עולמכם!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aעולמכם יקבע באופן אוטומטי כעולם הראשי כאשר השרת נפתח מחדש.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eיצירת העולם של Runtime זמינה Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eהכנת קבצים בעולם bukkit.yml עבור הסטארט-אפ הבא...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cנכשלים בקבצי עולם הבמה לממד \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aעדכון server.properties שם ברמה \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cהעולם הוטבע, אך לא הצליח לעדכן server.properties העולם הראשי",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aעולם Iris \"{name}\" הוכן עם המחולל Iris:{value} והזרע {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aהעולם הזה מוגדר כעת כעיקרי לחידוש הבא.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cנכשל לעדכן bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cהשחקן שצוין אינו קיים.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aאיריס v{value} על ידי Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "שם העולם ליצור",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "המימד / החבילה ליצירת העולם עם",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "הזרע ליצור את העולם עם",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "בין אם להשתמש בעולם באופן אוטומטי כעולם הראשי",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "החלפת משבצת העולם הקיימת המדויקת בהפעלה מחדש הבאה",
|
||||
"iris.director.commandiris.director.teleport_another_world": "טלפורט לעולם אחר",
|
||||
"iris.director.commandiris.param.world_teleport": "העולם לטלפורט",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cPossiamo suggerire il nome \"IrisWorld\" invece?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cQuella cartella esiste già!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProva una delle seguenti opzioni: overworld, vanilla, flat, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aPreparazione del mondo completata. Riavvia 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.successfully_created_your_world": "§aCreato con successo il tuo mondo!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aIl tuo mondo verrà impostato automaticamente come mondo principale al riavvio del server.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eLa creazione di mondi durante l'esecuzione è disattivata su Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§ePreparazione dei file del mondo e di bukkit.yml per il prossimo avvio...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cImpossibile preparare i file del mondo per la dimensione \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aLa proprietà level-name in server.properties è stata impostata su \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cIl mondo è stato preparato, ma non è stato possibile aggiornare il mondo principale in server.properties.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aMondo Iris \"{name}\" preparato con il generatore Iris:{value} e il seed {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aQuesto mondo è ora configurato come principale per il prossimo riavvio.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cImpossibile aggiornare bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cIl giocatore specificato non esiste.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} di Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Il nome del mondo da creare",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "La dimensione o il Pack con cui creare il mondo",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Il seed con cui generare il mondo",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Che sia o meno utilizzare automaticamente questo mondo come il mondo principale",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Sostituisci lo slot esatto del mondo esistente al prossimo riavvio",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teletrasporto in un altro mondo",
|
||||
"iris.director.commandiris.param.world_teleport": "Il mondo verso cui teletrasportarsi",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c代わりに \"IrisWorld\" という名前はいかがですか?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cそのフォルダーはすでに存在します!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e次のいずれかを指定してください: overworld、vanilla、flat、theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aワールドの準備が完了しました。\"{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.successfully_created_your_world": "§aワールドを作成しました!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aサーバーを再起動すると、このワールドが自動的にメインワールドに設定されます。",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eFolia では実行中のワールド作成が無効です。",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e次回起動用のワールドファイルと bukkit.yml を準備しています...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cディメンション \"{value}\" のワールドファイルを準備できませんでした。",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aserver.properties の level-name を \"{name}\" に更新しました。",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cワールドは準備されましたが、server.properties のメインワールド設定を更新できませんでした。",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aIris ワールド \"{name}\" をジェネレーター Iris:{value}、シード {seed} で準備しました。",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aこのワールドは、次回再起動時のメインワールドとして設定されました。",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cbukkit.yml を更新できませんでした: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定されたプレイヤーは存在しません。",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} by Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "作成するワールドの名前",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "ワールドの作成に使用するディメンションまたはパック",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "ワールドの生成に使用するシード",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "このワールドをメインワールドとして自動設定するかどうか",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "次回の再起動時に既存の正確なワールドスロットを置き換える",
|
||||
"iris.director.commandiris.director.teleport_another_world": "別のワールドへテレポートします",
|
||||
"iris.director.commandiris.param.world_teleport": "テレポート先のワールド",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c우리는 이름 \"을 제안 할 수있다IrisWorld\" 대신?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c그 폴더는 이미 존재합니다!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e중 하나를 시도: overworld, 바닐라, 플랫, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§a월드 준비가 완료되었습니다. \"{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.successfully_created_your_world": "§a세상을 성공적으로 만들었습니다!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§a서버가 재시작할 때 세계로 자동 설정됩니다.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eRuntime 세계 생성은 비활성화됩니다 Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e세계 파일 및 준비 bukkit.yml 다음 시작...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§c차원을 위한 단계 세계 파일에 실패 \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§a으로 server.properties \"에 수평 이름{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§c세계는 무대로, 하지만 업데이트 실패 server.properties 주요 세계.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aIris 월드 \"{name}\"을 생성기 Iris:{value}, 시드 {seed}로 준비했습니다.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§a다음 재시작의 메인 월드로 설정했습니다.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§c업데이트 실패 bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c지정된 플레이어는 존재하지 않습니다.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§a아이리스 v{value} Volmit 소프트웨어",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "만들 월드 이름",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "월드 생성에 사용할 차원 또는 팩",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "월드 생성에 사용할 시드",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "이 월드를 메인 월드로 자동 사용할지 여부",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "다음 재시작 시 기존의 정확한 월드 슬롯 교체",
|
||||
"iris.director.commandiris.director.teleport_another_world": "다른 월드로 순간이동합니다",
|
||||
"iris.director.commandiris.param.world_teleport": "순간이동할 월드",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cAr galime pasiūlyti pavadinimą \"IrisWorld\"vietoj to?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cTas aplankas jau egzistuoja!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePabandykite vieną iš: per pasaulį, vanilla, butas, pabaiga",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aPasaulio paruošimas baigtas. Paleiskite serverį iš naujo, kad sugeneruotumėte / įkeltumėte „{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.successfully_created_your_world": "§aSėkmingai sukūrė savo pasaulį!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aJūsų pasaulis bus automatiškai nustatytas kaip pagrindinis pasaulis, kai serveris vėl paleidžiamas.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eweather forecast Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eRuošiami pasauliniai failai ir bukkit.yml kitam paleidimui...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cNepavyko sukurti pasaulinių matmenų failų \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aAtnaujinta server.properties Lyginamasis pavadinimas į \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cPasaulis buvo suskirstytas, bet nepavyko atnaujinti server.properties pagrindinis pasaulis.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aParuoštas Iris pasaulis „{name}“ su generatoriumi Iris:{value} ir pradine reikšme {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aŠis pasaulis dabar yra sukonfigūruotas kaip pagrindinis kitą startą.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cNepavyko atnaujinti bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cNurodytasis žaidėjas neegzistuoja.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris prieš{value} pagal Volmit programinė įranga",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Kuriamo pasaulio pavadinimas",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "dimensija / paketas sukurti pasaulį su",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Sėkla generuoti pasaulį su",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Ar automatiškai naudoti šį pasaulį kaip pagrindinį pasaulį",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Per kitą paleidimą iš naujo pakeisti tikslų esamą pasaulio lizdą",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teleportas į kitą pasaulį",
|
||||
"iris.director.commandiris.param.world_teleport": "Pasaulis teleportui į",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cMogen we de naam voorstellen \"IrisWorldIn plaats daarvan?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cDie map bestaat al!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProbeer een van: overwereld, vanilla, plat, einde",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aVoorbereiding van de wereld voltooid. Herstart de server 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.successfully_created_your_world": "§aSuccesvol jullie wereld gecreëerd!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aUw wereld wordt automatisch ingesteld als de belangrijkste wereld wanneer de server herstart.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eRuntime wereld creatie is uitgeschakeld op Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eVoorbereiden van wereldbestanden en bukkit.yml voor volgende opstart...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cKon wereldbestanden voor dimensie niet stagen \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aBijgewerkt server.properties niveau-naam aan \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cWereld werd opgevoerd, maar kon niet bijwerken server.properties De belangrijkste wereld.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aIris-wereld \"{name}\" voorbereid met generator Iris:{value} en seed {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aDeze wereld is nu geconfigureerd als hoofd voor volgende herstart.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cBijwerken is mislukt bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cDe opgegeven speler bestaat niet.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris /{value} door Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "De naam van de wereld te creëren",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "De dimensie/pack om de wereld te creëren met",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Het zaad om de wereld te genereren met",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Of deze wereld automatisch gebruikt moet worden als de belangrijkste wereld",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "De exacte bestaande wereldsleuf bij de volgende herstart vervangen",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teleporteren naar een andere wereld",
|
||||
"iris.director.commandiris.param.world_teleport": "Wereld te teleporteren naar",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cCzy możemy zasugerować nazwę \"IrisWorld\"zamiast?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cTen folder już istnieje!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eSpróbuj jeden z: zaświaty, vanilla, płaski, koniec",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aPrzygotowanie świata ukończone. Uruchom 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.successfully_created_your_world": "§aZ powodzeniem stworzyłeś swój świat!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aTwój świat zostanie automatycznie ustawiony jako główny świat po ponownym uruchomieniu serwera.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eRuntime world creation jest wyłączone na Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§ePrzygotowanie światowych plików i bukkit.yml dla następnego startup...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cNie udało się sscenizować plików świata dla wymiaru \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aAktualizacja server.properties level- name do \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cŚwiat został wystawiony, ale nie udało się zaktualizować server.properties Główny świat.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aPrzygotowano świat Iris „{name}” z generatorem Iris:{value} i ziarnem {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aTen świat jest teraz skonfigurowany jako główny dla następnego restartu.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cNie udało się zaktualizować bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cOkreślony gracz nie istnieje.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value} przez Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Nazwa świata do stworzenia",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Wymiar / pakiet do tworzenia świata z",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Nasienie do generowania świata z",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Czy automatycznie używać tego świata jako głównego świata",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Zastąp dokładny istniejący slot świata przy następnym restarcie",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teleport do innego świata",
|
||||
"iris.director.commandiris.param.world_teleport": "Świat teleportować do",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cPodemos sugerir o nome \"IrisWorld\" em vez disso?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cEssa pasta já existe!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eExperimente um de: overworld, vanilla, flat, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aPreparação do mundo concluída. Reinicie 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.successfully_created_your_world": "§aCriou com sucesso o seu mundo!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aSeu mundo será automaticamente definido como o mundo principal quando o servidor reiniciar.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eA criação do mundo em execução está desactivada Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§ePreparando arquivos mundiais e bukkit.yml para a próxima inicialização...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cFalha ao encenar arquivos mundiais para a dimensão \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aAtualizado server.properties nível-nome para \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cO mundo foi encenado, mas falhou em atualizar server.properties mundo principal.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aMundo Iris \"{name}\" preparado com o gerador Iris:{value} e a seed {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aEste mundo está agora configurado como principal para o próximo reinício.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cFalha ao atualizar bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cO jogador especificado não existe.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aÍris v{value} por Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "O nome do mundo para criar",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "A dimensão/pack para criar o mundo com",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "A semente para gerar o mundo com",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Se deve ou não usar automaticamente este mundo como o mundo principal",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Substituir o espaço exato do mundo existente no próximo reinício",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teletransporte para outro mundo",
|
||||
"iris.director.commandiris.param.world_teleport": "Mundo para teletransportar",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cМожно предложить название \"IrisWorldВместо этого?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cЭта папка уже существует!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eПопробуйте один из вариантов: overworld, vanilla, flat, theend",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aПодготовка мира завершена. Перезапустите сервер, чтобы сгенерировать/загрузить «{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.successfully_created_your_world": "§aУспешно создал свой мир!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aВаш мир будет автоматически установлен в качестве основного мира при перезагрузке сервера.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "Создание мира §eRuntime отключено на Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eПодготовка мировых файлов и bukkit.yml Для следующего стартапа...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cНе удалось создать мировые файлы для измерения\".{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aОбновлено server.properties имя уровня \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cМир был инсценирован, но не удалось обновить server.properties Главный мир.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aМир Iris «{name}» подготовлен с генератором Iris:{value} и сидом {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aЭтот мир теперь настроен как основной для следующего перезапуска.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cНе удалось обновить bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cУказанного игрока не существует.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aИрис v{value} Разработчик Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Название мира для создания",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Размер/пакет для создания мира",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Семя, чтобы создать мир с",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Использовать или не использовать этот мир как основной",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Заменить точный существующий слот мира при следующем перезапуске",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Телепорт в другой мир",
|
||||
"iris.director.commandiris.param.world_teleport": "Телепортироваться в мир",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cAdımı önerebiliriz \"IrisWorldBunun yerine?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cBu klasör zaten var!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eBirini deneyin: Overworld, vanilla, düz, son",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aDünya hazırlığı tamamlandı. \"{worldName}\" dünyasını oluşturmak/yüklemek için sunucuyu yeniden başlatın.",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load": "§aDünya hazırlığı tamamlandı. Iris, \"{worldName}\" dünyasını oluşturmak/yüklemek için sunucuyu yeniden başlatıyor.",
|
||||
"iris.bukkit.commandiris.exception_raised_during_creation_see_console_more_details": "§cYaratılış sırasında ortaya çıktı. Konsolu daha fazla ayrıntı için görün.",
|
||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aBaşarılı bir şekilde dünyanızı yarattı!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aDünyanız otomatik olarak sunucu yeniden başladığında ana dünya olarak belirlenecektir.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eRuntime dünya yaratımı engellidir Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eDünya dosyalarını hazırlamak ve bukkit.yml Bir sonraki başlangıç için...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cBoyut için dünya dosyalarını sahnelemek için başarısız oldu \"{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aGüncelleme Tarihi server.properties Seviye adı \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cDünya sahnelendi, ancak güncellemeyi başaramadı server.properties Ana dünya.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aIris dünyası \"{name}\", Iris:{value} jeneratörü ve {seed} tohumu ile hazırlandı.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aBu dünya şimdi bir sonraki yeniden başlamak için ana olarak yapılandırılmıştır.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cgüncellemek için başarısız oldu bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cBelirtilen oyuncu mevcut değildir.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris{value} Volmit Software",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Dünyanın adı yaratmak",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Dünyayı yaratmak için boyut / paket",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Dünyayı üretmek için tohum",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Bu dünyayı ana dünya olarak otomatik olarak kullanıp kullanmayalım",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Bir sonraki yeniden başlatmada mevcut tam dünya yuvasını değiştir",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Teleport başka bir dünyaya",
|
||||
"iris.director.commandiris.param.world_teleport": "Dünya teleport'a",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§cChúng ta có thể đề nghị cái tên \"IrisWorldThay vào đó?",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cThư mục đó đã có!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eHãy thử một trong những: ngoài thế giới, vanilla, bằng phẳng, kết thúc",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§aĐã chuẩn bị xong thế giới. Hãy 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.successfully_created_your_world": "§aThành công trong việc tạo ra thế giới của anh!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§aThế giới của bạn sẽ tự động được thiết lập như thế giới chính khi máy chủ khởi động lại.",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eComment Folia.",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§eĐang chuẩn bị tập tin thế giới và bukkit.yml cho lần khởi chạy tiếp theo...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cKhông thể tạo tập tin cho chiều không gian{value}\".",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§aCập nhật server.properties Cấp-tên là \"{name}\".",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§cThế giới đã được thiết lập, nhưng không cập nhật được server.properties thế giới chính.",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§aĐã chuẩn bị thế giới Iris \"{name}\" với trình tạo Iris:{value} và seed {seed}.",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§aThế giới này bây giờ được cấu hình là chính cho lần khởi động lại tiếp theo.",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§cLỗi cập nhật bukkit.yml: {value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cNgười chơi đã ghi rõ không tồn tại.",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value} by Volmit Phần mềm",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "Tên của thế giới để tạo ra",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Kích thước/ lốc tạo ra thế giới với",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "Hạt giống để tạo ra thế giới với",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "Có nên tự động sử dụng thế giới này làm thế giới chính hay không",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "Thay thế đúng vị trí thế giới hiện có vào lần khởi động lại tiếp theo",
|
||||
"iris.director.commandiris.director.teleport_another_world": "Name",
|
||||
"iris.director.commandiris.param.world_teleport": "Thế giới có thể dịch chuyển",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c建议改用名称 \"IrisWorld\"。",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c该文件夹已存在!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可尝试:overworld、vanilla、flat 或 the_end",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§a世界暂存完成。请重启服务器以生成并加载 \"{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.successfully_created_your_world": "§a世界创建成功!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§a服务器重启时,此世界将自动设为主世界。",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eFolia 上已禁用运行时世界创建。",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e正在为下次启动准备世界文件和 bukkit.yml...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§c无法为维度 \"{value}\" 暂存世界文件。",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§a已将 server.properties 中的 level-name 更新为 \"{name}\"。",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§c世界已暂存,但无法更新 server.properties 中的主世界。",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§a已暂存 Iris 世界 \"{name}\",生成器为 Iris:{value},种子为 {seed}。",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§a此世界现已设为下次重启后的主世界。",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§c无法更新 bukkit.yml:{value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定的玩家不存在。",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value},由 Volmit Software 开发",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "要创建的世界名称",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "用于创建世界的维度包",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "用于生成世界的种子",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "是否自动将此世界设为主世界",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "在下次重启时替换指定的现有世界槽位",
|
||||
"iris.director.commandiris.director.teleport_another_world": "传送到另一个世界",
|
||||
"iris.director.commandiris.param.world_teleport": "要传送到的世界",
|
||||
|
||||
@@ -149,17 +149,13 @@
|
||||
"iris.bukkit.commandiris.may_we_suggest_name_irisworld_instead_2": "§c建議改用名稱 \"IrisWorld\"。",
|
||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c該資料夾已存在!",
|
||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可嘗試:overworld、vanilla、flat 或 the_end",
|
||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load": "§a世界暫存完成。請重新啟動伺服器以生成並載入 \"{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.successfully_created_your_world": "§a世界建立成功!",
|
||||
"iris.bukkit.commandiris.your_world_will_automatically_be_set_as_main_world_when": "§a伺服器重新啟動時,此世界將自動設為主世界。",
|
||||
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia": "§eFolia 上已停用執行階段世界建立。",
|
||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e正在為下次啟動準備世界檔案和 bukkit.yml...",
|
||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§c無法為維度 \"{value}\" 暫存世界檔案。",
|
||||
"iris.bukkit.commandiris.updated_server_properties_level_name": "§a已將 server.properties 中的 level-name 更新為 \"{name}\"。",
|
||||
"iris.bukkit.commandiris.world_was_staged_but_failed_update_server_properties_main_world": "§c世界已暫存,但無法更新 server.properties 中的主世界。",
|
||||
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed": "§a已暫存 Iris 世界 \"{name}\",生成器為 Iris:{value},種子為 {seed}。",
|
||||
"iris.bukkit.commandiris.this_world_is_now_configured_as_main_next_restart": "§a此世界現已設為下次重新啟動後的主世界。",
|
||||
"iris.bukkit.commandiris.failed_update_bukkit_yml": "§c無法更新 bukkit.yml:{value}",
|
||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定的玩家不存在。",
|
||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value},由 Volmit Software 開發",
|
||||
@@ -693,7 +689,6 @@
|
||||
"iris.director.commandiris.param.name_world_create": "要建立的世界名稱",
|
||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "用於建立世界的維度包",
|
||||
"iris.director.commandiris.param.seed_generate_world_with": "用於生成世界的種子",
|
||||
"iris.director.commandiris.param.whether_not_automatically_use_this_world_as_main_world": "是否自動將此世界設為主世界",
|
||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart": "在下次重新啟動時取代指定的現有世界槽位",
|
||||
"iris.director.commandiris.director.teleport_another_world": "傳送到另一個世界",
|
||||
"iris.director.commandiris.param.world_teleport": "要傳送到的世界",
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class ExactWorldSlotPathPolicyTest {
|
||||
@@ -53,6 +54,7 @@ public class ExactWorldSlotPathPolicyTest {
|
||||
assertEquals(canonicalRoot, target.levelRoot());
|
||||
assertEquals(canonicalRoot.resolve(expectation.relativePath()), target.worldDirectory());
|
||||
}
|
||||
assertFalse(Files.exists(canonicalRoot.resolve("dimensions")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -79,6 +80,23 @@ public class IrisDatapackCompilerInputFingerprintTest {
|
||||
assertNotEquals(initial, fingerprint(pack, false, "compiler-b"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredLevelStemBindingsSaltTheFingerprint() throws Exception {
|
||||
Path pack = activePack("binding-inputs");
|
||||
IrisGeneratorBinding moon = binding("moon", "overworld");
|
||||
IrisGeneratorBinding mars = binding("mars", "overworld");
|
||||
String empty = fingerprint(pack, List.of(), false, "compiler-a");
|
||||
String moonOnly = fingerprint(pack, List.of(moon), false, "compiler-a");
|
||||
String both = fingerprint(pack, List.of(mars, moon), false, "compiler-a");
|
||||
|
||||
assertNotEquals(empty, moonOnly);
|
||||
assertNotEquals(moonOnly, both);
|
||||
assertEquals(
|
||||
both,
|
||||
fingerprint(pack, List.of(moon, mars), false, "compiler-a")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compilerInputsRejectNestedSymbolicLinks() throws Exception {
|
||||
Path pack = activePack("unsafe-input");
|
||||
@@ -146,6 +164,7 @@ public class IrisDatapackCompilerInputFingerprintTest {
|
||||
assertEquals(compilerRoots, compilationRoots);
|
||||
assertTrue(!IrisDatapackCompiler.computeInputFingerprint(
|
||||
compilerRoots,
|
||||
List.of(),
|
||||
false,
|
||||
"compiler-a").isBlank());
|
||||
}
|
||||
@@ -163,9 +182,27 @@ public class IrisDatapackCompilerInputFingerprintTest {
|
||||
}
|
||||
|
||||
private String fingerprint(Path pack, boolean adjustVanillaHeight, String compilerIdentity) throws IOException {
|
||||
return fingerprint(pack, List.of(), adjustVanillaHeight, compilerIdentity);
|
||||
}
|
||||
|
||||
private String fingerprint(
|
||||
Path pack,
|
||||
List<IrisGeneratorBinding> bindings,
|
||||
boolean adjustVanillaHeight,
|
||||
String compilerIdentity
|
||||
) throws IOException {
|
||||
return IrisDatapackCompiler.computeInputFingerprint(
|
||||
List.of(pack.toFile()),
|
||||
bindings,
|
||||
adjustVanillaHeight,
|
||||
compilerIdentity);
|
||||
}
|
||||
|
||||
private IrisGeneratorBinding binding(String worldKey, String dimension) {
|
||||
return new IrisGeneratorBinding(
|
||||
"world_iris_" + worldKey,
|
||||
new WorldSlotKey("iris", worldKey),
|
||||
dimension
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -18,6 +19,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDatapackCompilerTest {
|
||||
@@ -42,6 +44,7 @@ public class IrisDatapackCompilerTest {
|
||||
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
||||
packRoots,
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
@@ -72,6 +75,7 @@ public class IrisDatapackCompilerTest {
|
||||
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
||||
List.of(packRoot.toFile()),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
@@ -106,6 +110,7 @@ public class IrisDatapackCompilerTest {
|
||||
IrisDatapackCompiler.compile(
|
||||
List.of(),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
@@ -136,6 +141,7 @@ public class IrisDatapackCompilerTest {
|
||||
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
||||
List.of(),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
@@ -147,7 +153,112 @@ public class IrisDatapackCompilerTest {
|
||||
assertFalse(Files.exists(stale));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emitsBootNativeCustomLevelStemBinding() throws Exception {
|
||||
Path packRoot = temporaryFolder.newFolder("binding-pack").toPath();
|
||||
Path datapackRoot = temporaryFolder.newFolder("binding-datapack").toPath();
|
||||
createPack(packRoot, "moon_pack", "moon_custom");
|
||||
|
||||
IrisDatapackCompiler.compile(
|
||||
List.of(packRoot.toFile()),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(binding("moon", "moon_pack")),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
|
||||
Path levelStemPath = datapackRoot.resolve("data/iris/dimension/moon.json");
|
||||
JSONObject levelStem = new JSONObject(Files.readString(levelStemPath, StandardCharsets.UTF_8));
|
||||
JSONObject generator = levelStem.getJSONObject("generator");
|
||||
JSONObject settings = generator.getJSONObject("settings");
|
||||
assertEquals("iris:moon_pack", levelStem.getString("type"));
|
||||
assertEquals("minecraft:flat", generator.getString("type"));
|
||||
assertEquals("minecraft:the_void", settings.getString("biome"));
|
||||
assertFalse(settings.getBoolean("features"));
|
||||
assertFalse(settings.getBoolean("lakes"));
|
||||
assertEquals(1, settings.getJSONArray("layers").length());
|
||||
assertEquals(
|
||||
"minecraft:air",
|
||||
settings.getJSONArray("layers").getJSONObject(0).getString("block")
|
||||
);
|
||||
assertEquals(1, settings.getJSONArray("layers").getJSONObject(0).getInt("height"));
|
||||
assertEquals(0, settings.getJSONArray("structure_overrides").length());
|
||||
assertFalse(Files.exists(datapackRoot.resolve("data/minecraft/dimension/overworld.json")));
|
||||
assertFalse(Files.exists(datapackRoot.resolve("data/minecraft/dimension/the_nether.json")));
|
||||
assertFalse(Files.exists(datapackRoot.resolve("data/minecraft/dimension/the_end.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsBindingToMissingDimension() throws Exception {
|
||||
Path packRoot = temporaryFolder.newFolder("missing-binding-pack").toPath();
|
||||
Path datapackRoot = temporaryFolder.newFolder("missing-binding-datapack").toPath();
|
||||
createPack(packRoot, "available", "available_custom");
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> IrisDatapackCompiler.compile(
|
||||
List.of(packRoot.toFile()),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(binding("moon", "missing")),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("selects missing dimension \"missing\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsBindingToConflictingDuplicateDimension() throws Exception {
|
||||
Path firstPack = temporaryFolder.newFolder("ambiguous-binding-first").toPath();
|
||||
Path secondPack = temporaryFolder.newFolder("ambiguous-binding-second").toPath();
|
||||
Path datapackRoot = temporaryFolder.newFolder("ambiguous-binding-datapack").toPath();
|
||||
createPack(firstPack, "shared", "first_custom", "NORMAL");
|
||||
createPack(secondPack, "shared", "second_custom", "NETHER");
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> IrisDatapackCompiler.compile(
|
||||
List.of(firstPack.toFile(), secondPack.toFile()),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(binding("moon", "shared")),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("selects ambiguous dimension \"shared\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsIdenticalFrozenAndInstalledDimensionDefinitions() throws Exception {
|
||||
Path installedPack = temporaryFolder.newFolder("identical-binding-installed").toPath();
|
||||
Path frozenPack = temporaryFolder.newFolder("identical-binding-frozen").toPath();
|
||||
Path datapackRoot = temporaryFolder.newFolder("identical-binding-datapack").toPath();
|
||||
createPack(installedPack, "shared", "installed_custom");
|
||||
createPack(frozenPack, "shared", "frozen_custom");
|
||||
|
||||
IrisDatapackCompiler.compile(
|
||||
List.of(installedPack.toFile(), frozenPack.toFile()),
|
||||
new KList<File>().qadd(datapackRoot.toFile()),
|
||||
List.of(binding("moon", "shared")),
|
||||
new DataFixerV1217(),
|
||||
false
|
||||
);
|
||||
|
||||
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/iris/dimension/moon.json")));
|
||||
}
|
||||
|
||||
private static void createPack(Path root, String dimensionKey, String biomeId) throws Exception {
|
||||
createPack(root, dimensionKey, biomeId, "NORMAL");
|
||||
}
|
||||
|
||||
private static void createPack(
|
||||
Path root,
|
||||
String dimensionKey,
|
||||
String biomeId,
|
||||
String environment
|
||||
) throws Exception {
|
||||
Files.createDirectories(root.resolve("dimensions"));
|
||||
Files.createDirectories(root.resolve("biomes"));
|
||||
Files.writeString(
|
||||
@@ -155,14 +266,14 @@ public class IrisDatapackCompilerTest {
|
||||
"""
|
||||
{
|
||||
"name": "Test Dimension",
|
||||
"environment": "NORMAL",
|
||||
"environment": "%s",
|
||||
"logicalHeight": 256,
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
}
|
||||
}
|
||||
""",
|
||||
""".formatted(environment),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
Files.writeString(
|
||||
@@ -182,4 +293,12 @@ public class IrisDatapackCompilerTest {
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
}
|
||||
|
||||
private static IrisGeneratorBinding binding(String worldKey, String dimension) {
|
||||
return new IrisGeneratorBinding(
|
||||
"world_iris_" + worldKey,
|
||||
new WorldSlotKey("iris", worldKey),
|
||||
dimension
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@@ -86,6 +87,23 @@ public class IrisStartupValidationTest {
|
||||
assertEquals("restart boundary", IrisStartupValidation.denialReason().orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartBoundaryAllowsReplacementStagingWithoutAllowingRuntimeCreation() {
|
||||
IrisStartupValidation.begin();
|
||||
IrisStartupValidation.markDatapacksReady();
|
||||
IrisStartupValidation.markPacksReady();
|
||||
IrisStartupValidation.requireRestart("restart boundary");
|
||||
|
||||
assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady);
|
||||
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||
|
||||
IrisStartupValidation.markPacksInvalid(List.of("pack validation failed"));
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
IrisStartupValidation::requireWorldReplacementStagingReady
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packValidationInfrastructureFailureDeniesCreation() {
|
||||
IrisStartupValidation.begin();
|
||||
|
||||
@@ -12,7 +12,9 @@ import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisWorldStorageTest {
|
||||
@Rule
|
||||
@@ -45,6 +47,113 @@ public class IrisWorldStorageTest {
|
||||
assertEquals(new NamespacedKey("iris", "iris_world"), IrisWorldStorage.keyFromName("Iris World", "world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsPaperStartupNamesWithoutChangingLogicalNames() {
|
||||
NamespacedKey worldKey = new NamespacedKey("iris", "moon");
|
||||
|
||||
assertEquals("world_iris_moon", IrisWorldStorage.configuredWorldName(worldKey, "world"));
|
||||
assertEquals(
|
||||
"world_iris_moon",
|
||||
IrisWorldStorage.configuredWorldName(new WorldSlotKey("iris", "moon"), "world")
|
||||
);
|
||||
assertEquals(worldKey, IrisWorldStorage.keyFromConfiguredWorldName("world_iris_moon", "world"));
|
||||
assertEquals("moon", IrisWorldStorage.logicalName(worldKey, "world"));
|
||||
assertEquals(NamespacedKey.minecraft("overworld"),
|
||||
IrisWorldStorage.keyFromConfiguredWorldName("world", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("the_nether"),
|
||||
IrisWorldStorage.keyFromConfiguredWorldName("world_nether", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("the_end"),
|
||||
IrisWorldStorage.keyFromConfiguredWorldName("world_the_end", "world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredWorldNameRejectsUnsafeManagedKeys() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisWorldStorage.keyFromConfiguredWorldName("world_iris_nested/world", "world"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisWorldStorage.configuredWorldName(new NamespacedKey("other", "moon"), "world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementKeyAcceptsExplicitCanonicalIdentities() {
|
||||
assertEquals(
|
||||
NamespacedKey.minecraft("overworld"),
|
||||
IrisWorldStorage.replacementKeyFromName("minecraft:overworld", "world")
|
||||
);
|
||||
assertEquals(
|
||||
NamespacedKey.minecraft("the_nether"),
|
||||
IrisWorldStorage.replacementKeyFromName("MINECRAFT:THE_NETHER", "world")
|
||||
);
|
||||
assertEquals(
|
||||
NamespacedKey.minecraft("the_end"),
|
||||
IrisWorldStorage.replacementKeyFromName("minecraft:the_end", "world")
|
||||
);
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "moon"),
|
||||
IrisWorldStorage.replacementKeyFromName("iris:moon", "world")
|
||||
);
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "main"),
|
||||
IrisWorldStorage.replacementKeyFromName("iris:main", "world")
|
||||
);
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "survival"),
|
||||
IrisWorldStorage.replacementKeyFromName("iris:survival", "survival")
|
||||
);
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "moon"),
|
||||
IrisWorldStorage.replacementKeyFromName("world_iris_moon", "world")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementKeyResolvesFriendlyVanillaAliases() {
|
||||
assertEquals(NamespacedKey.minecraft("overworld"),
|
||||
IrisWorldStorage.replacementKeyFromName("main", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("overworld"),
|
||||
IrisWorldStorage.replacementKeyFromName("overworld", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("the_nether"),
|
||||
IrisWorldStorage.replacementKeyFromName("nether", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("the_nether"),
|
||||
IrisWorldStorage.replacementKeyFromName("the_nether", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("the_end"),
|
||||
IrisWorldStorage.replacementKeyFromName("end", "world"));
|
||||
assertEquals(NamespacedKey.minecraft("the_end"),
|
||||
IrisWorldStorage.replacementKeyFromName("the_end", "world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementKeyResolvesConfiguredVanillaNamesBeforeFriendlyAliases() {
|
||||
assertEquals(NamespacedKey.minecraft("overworld"),
|
||||
IrisWorldStorage.replacementKeyFromName("survival", "survival"));
|
||||
assertEquals(NamespacedKey.minecraft("the_nether"),
|
||||
IrisWorldStorage.replacementKeyFromName("survival_nether", "survival"));
|
||||
assertEquals(NamespacedKey.minecraft("the_end"),
|
||||
IrisWorldStorage.replacementKeyFromName("survival_the_end", "survival"));
|
||||
assertEquals(NamespacedKey.minecraft("overworld"),
|
||||
IrisWorldStorage.replacementKeyFromName("nether", "nether"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementKeyMapsOtherBareNamesIntoIrisNamespace() {
|
||||
assertEquals(
|
||||
new NamespacedKey("iris", "moon_base"),
|
||||
IrisWorldStorage.replacementKeyFromName("Moon Base", "world")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replacementKeyRejectsUnsafePathsAndInvalidIdentifiers() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisWorldStorage.replacementKeyFromName("../world", "world"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisWorldStorage.replacementKeyFromName("iris:nested/world", "world"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisWorldStorage.replacementKeyFromName("iris:", "world"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisWorldStorage.replacementKeyFromName("world", " "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void managedKeyRejectsMainWorldsAndUnsafePaths() {
|
||||
assertEquals(new NamespacedKey("iris", "iris_world"),
|
||||
@@ -124,9 +233,26 @@ public class IrisWorldStorageTest {
|
||||
Path dimensions = Files.createDirectories(levelRoot.toPath().resolve("dimensions"));
|
||||
Path outside = temporaryFolder.newFolder("outside").toPath();
|
||||
Files.createSymbolicLink(dimensions.resolve("iris"), outside);
|
||||
NamespacedKey key = new NamespacedKey("iris", "probe");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> IrisWorldStorage.requireSafeManagedDimensionRoot(
|
||||
levelRoot,
|
||||
new NamespacedKey("iris", "probe")));
|
||||
key));
|
||||
assertFalse(IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingManagedDimensionRootRequiresRealDirectory() throws Exception {
|
||||
File levelRoot = temporaryFolder.newFolder("managed-world-admission");
|
||||
Path namespace = Files.createDirectories(levelRoot.toPath().resolve("dimensions/iris"));
|
||||
NamespacedKey missing = new NamespacedKey("iris", "missing");
|
||||
NamespacedKey file = new NamespacedKey("iris", "file");
|
||||
NamespacedKey directory = new NamespacedKey("iris", "directory");
|
||||
Files.writeString(namespace.resolve("file"), "not a directory");
|
||||
Files.createDirectory(namespace.resolve("directory"));
|
||||
|
||||
assertFalse(IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, missing));
|
||||
assertFalse(IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, file));
|
||||
assertTrue(IrisWorldStorage.isExistingManagedDimensionRoot(levelRoot, directory));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisWorldsTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void registryFileBelongsToSelectedLevelRoot() throws Exception {
|
||||
Path levelRoot = temporaryFolder.newFolder("world").toPath();
|
||||
|
||||
assertEquals(
|
||||
levelRoot.toAbsolutePath().normalize().resolve("iris/worlds.json"),
|
||||
IrisWorlds.registryFile(levelRoot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bukkitWorldFilteringUsesExactStorageInSelectedRoot() throws Exception {
|
||||
Path selectedRoot = temporaryFolder.newFolder("world").toPath();
|
||||
Path otherRoot = temporaryFolder.newFolder("archive").toPath();
|
||||
Files.createDirectories(selectedRoot.resolve("dimensions/minecraft/overworld"));
|
||||
Files.createDirectories(selectedRoot.resolve("dimensions/iris/moon"));
|
||||
Files.createDirectories(otherRoot.resolve("dimensions/iris/foreign"));
|
||||
Files.createDirectories(selectedRoot.resolve("dimensions/iris"));
|
||||
Files.writeString(selectedRoot.resolve("dimensions/iris/not_a_directory"), "not storage");
|
||||
|
||||
Map<String, String> configuredWorlds = new LinkedHashMap<>();
|
||||
configuredWorlds.put("world", "overworld");
|
||||
configuredWorlds.put("world_nether", "underworld");
|
||||
configuredWorlds.put("world_iris_moon", "overworld");
|
||||
configuredWorlds.put("moon", "overworld");
|
||||
configuredWorlds.put("world_iris_foreign", "overworld");
|
||||
configuredWorlds.put("archive_iris_foreign", "overworld");
|
||||
configuredWorlds.put("foreign", "overworld");
|
||||
configuredWorlds.put("world_iris_not_a_directory", "overworld");
|
||||
configuredWorlds.put("world_iris_missing", "overworld");
|
||||
|
||||
Map<String, String> selected = IrisWorlds.filterBukkitWorldsByStorage(selectedRoot, configuredWorlds);
|
||||
Map<String, String> other = IrisWorlds.filterBukkitWorldsByStorage(otherRoot, configuredWorlds);
|
||||
|
||||
assertEquals(Set.of("world", "world_iris_moon"), selected.keySet());
|
||||
assertEquals(Set.of("archive_iris_foreign"), other.keySet());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.WorldSlotKey;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -13,6 +14,7 @@ import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -26,6 +28,112 @@ public class BukkitWorldConfigurationTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void readsOnlyCanonicalCustomIrisGeneratorBindings() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("binding-bukkit.yml");
|
||||
Path levelRoot = temporaryFolder.newFolder("binding-level-root").toPath();
|
||||
Files.createDirectories(levelRoot.resolve("dimensions/iris/moon"));
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("worlds.world.generator", "Iris:overworld");
|
||||
yaml.set("worlds.world_nether.generator", "Iris:underworld");
|
||||
yaml.set("worlds.world_the_end.generator", "Iris:theend");
|
||||
yaml.set("worlds.moon.generator", "Iris:noncanonical_short_name");
|
||||
yaml.set("worlds.world_iris_moon.generator", "Iris:moon_pack");
|
||||
yaml.set("worlds.world_iris_other.generator", "Other:generator");
|
||||
yaml.save(configuration);
|
||||
|
||||
List<BukkitWorldConfiguration.IrisGeneratorBinding> bindings =
|
||||
BukkitWorldConfiguration.readIrisGeneratorBindings(configuration, "world", levelRoot);
|
||||
|
||||
assertEquals(List.of(new BukkitWorldConfiguration.IrisGeneratorBinding(
|
||||
"world_iris_moon",
|
||||
new WorldSlotKey("iris", "moon"),
|
||||
"moon_pack"
|
||||
)), bindings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsCustomIrisBindingWithoutSelectedDimension() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("missing-binding-dimension.yml");
|
||||
Path levelRoot = temporaryFolder.newFolder("missing-binding-level-root").toPath();
|
||||
Files.createDirectories(levelRoot.resolve("dimensions/iris/moon"));
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("worlds.world_iris_moon.generator", "Iris");
|
||||
yaml.save(configuration);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> BukkitWorldConfiguration.readIrisGeneratorBindings(configuration, "world", levelRoot)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("must select a dimension"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresNoncanonicalShortNamesThatCollapseOntoCanonicalIrisKeys() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("ambiguous-binding-name.yml");
|
||||
Path levelRoot = temporaryFolder.newFolder("ambiguous-binding-level-root").toPath();
|
||||
Files.createDirectories(levelRoot.resolve("dimensions/iris/moon"));
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("worlds.moon.generator", "Iris:moon_pack");
|
||||
yaml.set("worlds.world_iris_moon.generator", "Iris:moon_pack");
|
||||
yaml.save(configuration);
|
||||
|
||||
assertEquals(List.of(new BukkitWorldConfiguration.IrisGeneratorBinding(
|
||||
"world_iris_moon",
|
||||
new WorldSlotKey("iris", "moon"),
|
||||
"moon_pack"
|
||||
)), BukkitWorldConfiguration.readIrisGeneratorBindings(configuration, "world", levelRoot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void admitsOnlyExistingCustomWorldDirectoriesUnderSelectedLevelRoot() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("root-admission-bukkit.yml");
|
||||
Path selectedRoot = temporaryFolder.newFolder("selected-level-root").toPath();
|
||||
Path otherRoot = temporaryFolder.newFolder("other-level-root").toPath();
|
||||
Files.createDirectories(selectedRoot.resolve("dimensions/iris/moon"));
|
||||
Files.createDirectories(otherRoot.resolve("dimensions/iris/moon"));
|
||||
Files.createDirectories(selectedRoot.resolve("dimensions/iris"));
|
||||
Files.writeString(selectedRoot.resolve("dimensions/iris/file"), "not a world directory");
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("worlds.world_iris_moon.generator", "Iris:moon_pack");
|
||||
yaml.set("worlds.other_iris_moon.generator", "Iris:other_pack");
|
||||
yaml.set("worlds.world_iris_missing.generator", "Iris:missing_pack");
|
||||
yaml.set("worlds.world_iris_file.generator", "Iris:file_pack");
|
||||
yaml.save(configuration);
|
||||
|
||||
List<BukkitWorldConfiguration.IrisGeneratorBinding> bindings =
|
||||
BukkitWorldConfiguration.readIrisGeneratorBindings(configuration, "world", selectedRoot);
|
||||
|
||||
assertEquals(List.of(new BukkitWorldConfiguration.IrisGeneratorBinding(
|
||||
"world_iris_moon",
|
||||
new WorldSlotKey("iris", "moon"),
|
||||
"moon_pack"
|
||||
)), bindings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void excludesSymlinkedCustomWorldTarget() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("symlink-admission-bukkit.yml");
|
||||
Path levelRoot = temporaryFolder.newFolder("symlink-level-root").toPath();
|
||||
Path namespaceRoot = Files.createDirectories(levelRoot.resolve("dimensions/iris"));
|
||||
Path outside = temporaryFolder.newFolder("symlink-world-outside").toPath();
|
||||
try {
|
||||
Files.createSymbolicLink(namespaceRoot.resolve("moon"), outside);
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
assumeNoException(exception);
|
||||
}
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("worlds.world_iris_moon.generator", "Iris:moon_pack");
|
||||
yaml.save(configuration);
|
||||
|
||||
assertTrue(BukkitWorldConfiguration.readIrisGeneratorBindings(
|
||||
configuration,
|
||||
"world",
|
||||
levelRoot
|
||||
).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registersAndRemovesWorldAtomically() throws Exception {
|
||||
File configuration = temporaryFolder.newFile("bukkit.yml");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.core.WorldRemovalPathPolicy;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
@@ -18,6 +19,7 @@ import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -112,6 +114,34 @@ public class IrisWorldRemovalServiceTest {
|
||||
assertTrue(result.registryChanged());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bukkitUnregistrationUsesCanonicalPaperStartupName() throws Exception {
|
||||
Path levelRoot = temporaryFolder.newFolder("world").toPath();
|
||||
WorldRemovalPathPolicy.Target target = WorldRemovalPathPolicy.resolve("moon", "world", levelRoot);
|
||||
|
||||
assertEquals(
|
||||
"world_iris_moon",
|
||||
IrisWorldRemovalService.bukkitConfigurationWorldName(target)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diskInspectionReadsOnlyCanonicalPaperStartupSection() throws Exception {
|
||||
Path levelRoot = temporaryFolder.newFolder("inspection-world").toPath();
|
||||
WorldRemovalPathPolicy.Target target = WorldRemovalPathPolicy.resolve(
|
||||
"moon",
|
||||
"inspection-world",
|
||||
levelRoot
|
||||
);
|
||||
YamlConfiguration configuration = new YamlConfiguration();
|
||||
configuration.set("worlds.moon.generator", "Iris:noncanonical");
|
||||
|
||||
assertNull(IrisWorldRemovalService.bukkitGenerator(configuration, target));
|
||||
|
||||
configuration.set("worlds.inspection-world_iris_moon.generator", "Iris:overworld");
|
||||
assertEquals("Iris:overworld", IrisWorldRemovalService.bukkitGenerator(configuration, target));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registryFailureStopsBeforeFilesystemDeletion() throws Exception {
|
||||
LifecycleOperationCoordinator coordinator = new LifecycleOperationCoordinator();
|
||||
|
||||
+59
-17
@@ -238,6 +238,57 @@ public class WorldReplacementBootstrapTest {
|
||||
assertFalse(Files.exists(paths(first).backup()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishesTwoDistinctArmedReplacementsInOneColdReconcile() throws Exception {
|
||||
Transaction nether = stagedTransaction(Phase.ARMED, true, "nether-original");
|
||||
configureReplacement(nether);
|
||||
|
||||
WorldSlotKey endKey = WorldSlotKey.minecraft("the_end");
|
||||
ExactWorldSlotPathPolicy.Target endTarget = ExactWorldSlotPathPolicy.resolve(levelRoot, endKey);
|
||||
WorldGeneratorSnapshot endOriginal = BukkitWorldConfiguration.snapshot(
|
||||
bukkitConfiguration.toFile(),
|
||||
"world_the_end"
|
||||
);
|
||||
UUID endId = UUID.randomUUID();
|
||||
ReplacementPaths endPaths = WorldReplacementFilesystem.paths(endTarget, endId);
|
||||
writeOriginalTarget(endPaths, "end-original");
|
||||
Path endDimension = endPaths.stage().resolve("iris/pack/dimensions/theend.json");
|
||||
Files.createDirectories(endDimension.getParent());
|
||||
Files.writeString(endDimension, "end-replacement");
|
||||
String endFingerprint = WorldReplacementFilesystem.fingerprintPack(
|
||||
endPaths.stage().resolve("iris/pack")
|
||||
);
|
||||
Transaction end = new Transaction(
|
||||
endId,
|
||||
endKey,
|
||||
"world_the_end",
|
||||
endTarget.levelRoot(),
|
||||
"theend",
|
||||
SEED,
|
||||
endFingerprint,
|
||||
endOriginal,
|
||||
true,
|
||||
Phase.ARMED
|
||||
);
|
||||
WorldReplacementJournal.write(dataDirectory, end);
|
||||
configureReplacement(end);
|
||||
|
||||
WorldReplacementBootstrap.ReconcileResult result = reconcile();
|
||||
|
||||
assertEquals(2, result.transactions());
|
||||
assertEquals(2, result.published());
|
||||
assertEquals(0, result.rolledBack());
|
||||
assertEquals(0, result.retained());
|
||||
assertEquals("replacement", replacementContent(target.worldDirectory()));
|
||||
assertEquals("end-replacement", Files.readString(endTarget.worldDirectory()
|
||||
.resolve("iris/pack/dimensions/theend.json")));
|
||||
assertEquals("nether-original", Files.readString(backup(nether).resolve("original.txt")));
|
||||
assertEquals("end-original", Files.readString(endPaths.backup().resolve("original.txt")));
|
||||
List<Transaction> published = WorldReplacementJournal.load(dataDirectory, levelRoot);
|
||||
assertEquals(2, published.size());
|
||||
assertTrue(published.stream().allMatch(transaction -> transaction.phase() == Phase.PUBLISHED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripsBlankAndWhitespaceOriginalGenerators() throws Exception {
|
||||
for (String generator : List.of("", " ")) {
|
||||
@@ -260,24 +311,15 @@ public class WorldReplacementBootstrapTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsIrisWorldKeysThatCollideWithConfiguredVanillaAliases() {
|
||||
WorldGeneratorSnapshot original = new WorldGeneratorSnapshot(false, false, false, null, false, null);
|
||||
for (String alias : List.of("world", "world_nether", "world_the_end")) {
|
||||
Transaction transaction = new Transaction(
|
||||
UUID.randomUUID(),
|
||||
new WorldSlotKey("iris", alias),
|
||||
alias,
|
||||
levelRoot,
|
||||
"underworld",
|
||||
SEED,
|
||||
"0".repeat(64),
|
||||
original,
|
||||
false,
|
||||
Phase.ARMED
|
||||
public void usesPaperStartupAliasesForIrisReplacementJournals() {
|
||||
assertEquals(
|
||||
"world_iris_moon",
|
||||
WorldReplacementJournal.logicalWorldName(levelRoot, new WorldSlotKey("iris", "moon"))
|
||||
);
|
||||
assertEquals(
|
||||
"world_iris_world_nether",
|
||||
WorldReplacementJournal.logicalWorldName(levelRoot, new WorldSlotKey("iris", "world_nether"))
|
||||
);
|
||||
|
||||
assertThrows(IOException.class, () -> WorldReplacementJournal.resolveTarget(transaction, levelRoot));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+39
-3
@@ -17,6 +17,7 @@ import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -67,7 +68,10 @@ public class WorldReplacementFilesystemTest {
|
||||
() -> WorldReplacementFilesystem.requireExistingTarget(paths)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("requires an existing exact world slot"));
|
||||
assertEquals(
|
||||
"/iris replace requires an existing exact world slot; use /iris create for a new world.",
|
||||
failure.getMessage()
|
||||
);
|
||||
assertFalse(Files.exists(paths.target()));
|
||||
assertFalse(Files.exists(paths.stage()));
|
||||
assertFalse(Files.exists(paths.backup()));
|
||||
@@ -92,7 +96,7 @@ public class WorldReplacementFilesystemTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnmigratedRetainedWorldBeforePublication() throws Exception {
|
||||
public void rejectsIncompleteCurrentPaperWorldBeforePublication() throws Exception {
|
||||
WorldReplacementFilesystem.ReplacementPaths paths = paths("missing-paper-metadata", TRANSACTION_ID);
|
||||
Files.createDirectories(paths.target());
|
||||
Files.writeString(paths.target().resolve("original.txt"), "original");
|
||||
@@ -109,7 +113,7 @@ public class WorldReplacementFilesystemTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnmigratedRetainedWorldAtAdmission() throws Exception {
|
||||
public void rejectsIncompleteCurrentPaperWorldAtAdmission() throws Exception {
|
||||
WorldReplacementFilesystem.ReplacementPaths paths = paths("admit-missing-paper-metadata", TRANSACTION_ID);
|
||||
Files.createDirectories(paths.target());
|
||||
Files.writeString(paths.target().resolve("original.txt"), "original");
|
||||
@@ -331,6 +335,38 @@ public class WorldReplacementFilesystemTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresGeneratedAuthoringMetadataWithoutIgnoringNestedPackContent() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder("generated-metadata").toPath();
|
||||
Path objects = Files.createDirectories(pack.resolve("objects"));
|
||||
Files.writeString(objects.resolve("tree.iob"), "tree");
|
||||
String expected = WorldReplacementFilesystem.fingerprintPack(pack);
|
||||
|
||||
Files.createDirectories(pack.resolve(".iris/schema"));
|
||||
Files.writeString(pack.resolve(".iris/schema/dimensions-schema.json"), "schema");
|
||||
Files.createDirectories(pack.resolve(".idea"));
|
||||
Files.writeString(pack.resolve(".idea/jsonSchemas.xml"), "generated UUIDs");
|
||||
Files.writeString(pack.resolve("pack.code-workspace"), "workspace");
|
||||
Files.writeString(objects.resolve("editor.code-workspace"), "nested workspace");
|
||||
|
||||
assertEquals(expected, WorldReplacementFilesystem.fingerprintPack(pack));
|
||||
|
||||
Path nestedPackContent = Files.createDirectories(objects.resolve(".iris"));
|
||||
Files.writeString(nestedPackContent.resolve("semantic.json"), "pack content");
|
||||
|
||||
assertNotEquals(expected, WorldReplacementFilesystem.fingerprintPack(pack));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatesExcludedAuthoringMetadataForUnsafeEntries() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder("unsafe-generated-metadata").toPath();
|
||||
Path metadata = Files.createDirectories(pack.resolve(".iris"));
|
||||
Path outside = temporaryFolder.newFile("outside-generated-metadata.txt").toPath();
|
||||
Files.createSymbolicLink(metadata.resolve("linked.json"), outside);
|
||||
|
||||
assertThrows(IOException.class, () -> WorldReplacementFilesystem.fingerprintPack(pack));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedAndCrossTransactionPaths() throws Exception {
|
||||
Path parent = temporaryFolder.newFolder("invalid-paths").toPath();
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.volmlib.util.nbt.io.NBTUtil;
|
||||
import art.arcane.volmlib.util.nbt.io.NamedTag;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class WorldReplacementSeedTest {
|
||||
private static final long SEED = -734829104958217364L;
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void readsAuthoritativeLongSeed() throws Exception {
|
||||
CompoundTag data = new CompoundTag();
|
||||
data.putLong("seed", SEED);
|
||||
CompoundTag root = new CompoundTag();
|
||||
root.put("data", data);
|
||||
Path worldDirectory = writeSettings("valid", root);
|
||||
|
||||
assertEquals(SEED, WorldReplacementSeed.readAuthoritativeSeed(worldDirectory));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copiesSettingsAndRewritesOnlyAuthoritativeSeed() throws Exception {
|
||||
CompoundTag dimensions = new CompoundTag();
|
||||
dimensions.putString("marker", "preserved");
|
||||
CompoundTag data = new CompoundTag();
|
||||
data.putLong("seed", 1337L);
|
||||
data.putString("generator", "preserved");
|
||||
data.put("dimensions", dimensions);
|
||||
CompoundTag root = new CompoundTag();
|
||||
root.put("data", data);
|
||||
root.putString("root-marker", "preserved");
|
||||
Path sourceWorld = writeSettings("copy-source", root);
|
||||
Path targetWorld = temporaryFolder.newFolder("copy-target").toPath();
|
||||
|
||||
WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorld, targetWorld, SEED);
|
||||
|
||||
assertEquals(1337L, WorldReplacementSeed.readAuthoritativeSeed(sourceWorld));
|
||||
assertEquals(SEED, WorldReplacementSeed.readAuthoritativeSeed(targetWorld));
|
||||
NamedTag copied = NBTUtil.read(settingsPath(targetWorld).toFile());
|
||||
CompoundTag copiedRoot = (CompoundTag) copied.getTag();
|
||||
CompoundTag copiedData = copiedRoot.getCompoundTag("data");
|
||||
assertEquals("preserved", copiedRoot.getString("root-marker"));
|
||||
assertEquals("preserved", copiedData.getString("generator"));
|
||||
assertEquals("preserved", copiedData.getCompoundTag("dimensions").getString("marker"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingDataCompound() throws Exception {
|
||||
Path worldDirectory = writeSettings("missing-data", new CompoundTag());
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementSeed.readAuthoritativeSeed(worldDirectory)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("missing data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingSeed() throws Exception {
|
||||
CompoundTag root = new CompoundTag();
|
||||
root.put("data", new CompoundTag());
|
||||
Path worldDirectory = writeSettings("missing-seed", root);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementSeed.readAuthoritativeSeed(worldDirectory)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("missing data.seed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsWrongDataType() throws Exception {
|
||||
CompoundTag root = new CompoundTag();
|
||||
root.putString("data", "invalid");
|
||||
Path worldDirectory = writeSettings("wrong-data-type", root);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementSeed.readAuthoritativeSeed(worldDirectory)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("data must be a compound tag"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNonLongSeed() throws Exception {
|
||||
CompoundTag data = new CompoundTag();
|
||||
data.putInt("seed", 1337);
|
||||
CompoundTag root = new CompoundTag();
|
||||
root.put("data", data);
|
||||
Path worldDirectory = writeSettings("wrong-seed-type", root);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementSeed.readAuthoritativeSeed(worldDirectory)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("data.seed must be a long tag"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsCorruptSettings() throws Exception {
|
||||
Path worldDirectory = temporaryFolder.newFolder("corrupt").toPath();
|
||||
Path settings = settingsPath(worldDirectory);
|
||||
Files.createDirectories(settings.getParent());
|
||||
Files.write(settings, new byte[]{10, 0});
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> WorldReplacementSeed.readAuthoritativeSeed(worldDirectory)
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains("Could not read Paper world generation settings"));
|
||||
}
|
||||
|
||||
private Path writeSettings(String name, CompoundTag root) throws Exception {
|
||||
Path worldDirectory = temporaryFolder.newFolder(name).toPath();
|
||||
Path settings = settingsPath(worldDirectory);
|
||||
Files.createDirectories(settings.getParent());
|
||||
NBTUtil.write(root, settings.toFile());
|
||||
return worldDirectory;
|
||||
}
|
||||
|
||||
private Path settingsPath(Path worldDirectory) {
|
||||
return worldDirectory.resolve("data/minecraft/world_gen_settings.dat");
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class AtomicDirectoryPublisherTest {
|
||||
@@ -52,4 +54,21 @@ public class AtomicDirectoryPublisherTest {
|
||||
assertTrue(Files.isDirectory(target));
|
||||
assertEquals("old", Files.readString(target.resolve("value.txt")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentPublicationRefusesAnExistingTargetWithoutMovingEitherDirectory() throws Exception {
|
||||
Path root = temporaryFolder.getRoot().toPath();
|
||||
Path target = Files.createDirectory(root.resolve("existing-target"));
|
||||
Files.writeString(target.resolve("value.txt"), "old");
|
||||
Path staged = Files.createDirectory(root.resolve("absent-stage"));
|
||||
Files.writeString(staged.resolve("value.txt"), "new");
|
||||
|
||||
assertThrows(
|
||||
FileAlreadyExistsException.class,
|
||||
() -> AtomicDirectoryPublisher.publishAbsent(staged, target)
|
||||
);
|
||||
|
||||
assertEquals("old", Files.readString(target.resolve("value.txt")));
|
||||
assertEquals("new", Files.readString(staged.resolve("value.txt")));
|
||||
}
|
||||
}
|
||||
|
||||
+83
-5
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.Assume;
|
||||
@@ -49,6 +50,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
try {
|
||||
Path dataDirectory = root.resolve("plugins/Iris");
|
||||
DefaultPackBootstrapProvisioner.ProvisionOptions options = new DefaultPackBootstrapProvisioner.ProvisionOptions(
|
||||
List.of(),
|
||||
List.of(),
|
||||
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
|
||||
Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
|
||||
@@ -71,7 +73,12 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
assertTrue(result.packRoots().isEmpty());
|
||||
assertEquals(0, requests.get());
|
||||
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("pack.mcmeta")));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, List.of()));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(
|
||||
dataDirectory,
|
||||
root,
|
||||
List.of(),
|
||||
List.of()
|
||||
));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
delete(root);
|
||||
@@ -114,14 +121,24 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/overworld/worldgen/biome/bootstrap_biome.json")));
|
||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/underworld/worldgen/biome/underworld_biome.json")));
|
||||
assertFalse(Files.exists(dataDirectory.resolve("bootstrap/datapack")));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, options.packs()));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(
|
||||
dataDirectory,
|
||||
root,
|
||||
options.packs(),
|
||||
options.bindings()
|
||||
));
|
||||
assertTrue(DefaultPackBootstrapProvisioner.wasProvisionedThisStartup());
|
||||
Properties marker = loadProperties(dataDirectory.resolve("bootstrap/provisioned.properties"));
|
||||
assertEquals("true", marker.getProperty("pack.overworld.managed"));
|
||||
assertEquals("true", marker.getProperty("pack.underworld.managed"));
|
||||
assertEquals("underworld", marker.getProperty("pack.underworld.requiredDimension"));
|
||||
delete(installed.packRoots().get("underworld"));
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, options.packs()));
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(
|
||||
dataDirectory,
|
||||
root,
|
||||
options.packs(),
|
||||
options.bindings()
|
||||
));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
delete(root);
|
||||
@@ -309,7 +326,12 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("data/overworld/worldgen/biome/local_biome.json")));
|
||||
|
||||
Files.writeString(target.resolve("biomes/local.json"), biomeJson("changed_biome"), StandardCharsets.UTF_8);
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, options.packs()));
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(
|
||||
dataDirectory,
|
||||
root,
|
||||
options.packs(),
|
||||
options.bindings()
|
||||
));
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult updated = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
@@ -338,7 +360,12 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
writePack(dataDirectory.resolve("packs/second"), "second", "second_biome");
|
||||
writePack(root.resolve("dimensions/example/world/iris/pack"), "world_local", "world_local_biome");
|
||||
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, options.packs()));
|
||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(
|
||||
dataDirectory,
|
||||
root,
|
||||
options.packs(),
|
||||
options.bindings()
|
||||
));
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult updated = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
@@ -591,6 +618,56 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void effectiveStartupBindingsInstallAndRefreshBootNativeLevelStem() throws Exception {
|
||||
Path serverRoot = Files.createTempDirectory("iris-bootstrap-level-stem");
|
||||
try {
|
||||
Path dataDirectory = serverRoot.resolve("plugins/Iris");
|
||||
Path levelRoot = serverRoot.resolve("levels/primary");
|
||||
Path packRoot = levelRoot.resolve("dimensions/iris/moon/iris/pack");
|
||||
Files.createDirectories(levelRoot);
|
||||
Files.writeString(
|
||||
serverRoot.resolve("server.properties"),
|
||||
"level-name=levels/primary\n",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
writePack(packRoot, "overworld", "overworld_biome");
|
||||
Files.writeString(
|
||||
packRoot.resolve("dimensions/underworld.json"),
|
||||
dimensionJson("underworld"),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
Files.writeString(
|
||||
serverRoot.resolve("bukkit.yml"),
|
||||
"worlds:\n primary_iris_moon:\n generator: Iris:overworld\n",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(serverRoot, new String[0]);
|
||||
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult installed =
|
||||
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
|
||||
}, startupPaths);
|
||||
|
||||
Path levelStem = installed.datapackRoot().resolve("data/iris/dimension/moon.json");
|
||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
|
||||
assertTrue(Files.readString(levelStem).contains("\"type\": \"iris:overworld\""));
|
||||
|
||||
Files.writeString(
|
||||
serverRoot.resolve("bukkit.yml"),
|
||||
"worlds:\n primary_iris_moon:\n generator: Iris:underworld\n",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult updated =
|
||||
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
|
||||
}, startupPaths);
|
||||
|
||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, updated.status());
|
||||
assertTrue(Files.readString(levelStem).contains("\"type\": \"iris:underworld\""));
|
||||
} finally {
|
||||
delete(serverRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static DefaultPackBootstrapProvisioner.ProvisionOptions options(
|
||||
HttpServer server,
|
||||
Path serverRoot,
|
||||
@@ -610,6 +687,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
"underworld"
|
||||
)
|
||||
),
|
||||
List.of(),
|
||||
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
|
||||
Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
|
||||
refreshInterval,
|
||||
|
||||
Reference in New Issue
Block a user