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.Pair;
|
||||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
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.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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 art.arcane.iris.util.common.scheduling.J;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
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.ByteBuddy;
|
||||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
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.BoundingBox;
|
||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
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.AbstractHugeMushroomFeature;
|
||||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||||
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
||||||
@@ -151,10 +156,14 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Modifier;
|
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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -163,11 +172,18 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
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.Executor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class NMSBinding implements INMSBinding {
|
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 final KMap<Biome, Object> baseBiomeCache = new KMap<>();
|
||||||
private volatile DataVersion dataVersion;
|
private volatile DataVersion dataVersion;
|
||||||
private final BlockData AIR = Material.AIR.createBlockData();
|
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
|
@Override
|
||||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||||
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
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;
|
DimensionResolution dimensionResolution;
|
||||||
Long configuredSeed;
|
Long configuredSeed;
|
||||||
|
String configuredWorldName;
|
||||||
try {
|
try {
|
||||||
|
configuredWorldName = backend.configuredWorldName(worldKey);
|
||||||
dimensionResolution = backend.resolveDimension(worldKey);
|
dimensionResolution = backend.resolveDimension(worldKey);
|
||||||
configuredSeed = dimensionResolution.succeeded()
|
configuredSeed = dimensionResolution.succeeded()
|
||||||
? backend.configuredSeed(IrisWorldStorage.logicalName(worldKey))
|
? backend.configuredSeed(configuredWorldName)
|
||||||
: null;
|
: null;
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
dimensionResolution = DimensionResolution.failed(failure);
|
dimensionResolution = DimensionResolution.failed(failure);
|
||||||
configuredSeed = null;
|
configuredSeed = null;
|
||||||
|
configuredWorldName = IrisWorldStorage.logicalName(worldKey);
|
||||||
}
|
}
|
||||||
if (!dimensionResolution.succeeded()) {
|
if (!dimensionResolution.succeeded()) {
|
||||||
lease.close();
|
lease.close();
|
||||||
@@ -106,6 +109,7 @@ public final class BukkitWorldReconciler {
|
|||||||
}
|
}
|
||||||
return loadWithLease(
|
return loadWithLease(
|
||||||
configurationFile,
|
configurationFile,
|
||||||
|
configuredWorldName,
|
||||||
worldKey,
|
worldKey,
|
||||||
dimensionResolution.dimension(),
|
dimensionResolution.dimension(),
|
||||||
configuredSeed,
|
configuredSeed,
|
||||||
@@ -137,7 +141,7 @@ public final class BukkitWorldReconciler {
|
|||||||
}
|
}
|
||||||
NamespacedKey worldKey;
|
NamespacedKey worldKey;
|
||||||
try {
|
try {
|
||||||
worldKey = IrisWorldStorage.keyFromName(worldName);
|
worldKey = backend.worldKeyFromConfiguration(worldName);
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
chain = chain.thenApply(results -> {
|
chain = chain.thenApply(results -> {
|
||||||
results.add(LoadResult.validationFailure(worldName, failure));
|
results.add(LoadResult.validationFailure(worldName, failure));
|
||||||
@@ -158,6 +162,7 @@ public final class BukkitWorldReconciler {
|
|||||||
}
|
}
|
||||||
chain = chain.thenCompose(results -> loadConfiguredWorld(
|
chain = chain.thenCompose(results -> loadConfiguredWorld(
|
||||||
ServerProperties.BUKKIT_YML,
|
ServerProperties.BUKKIT_YML,
|
||||||
|
worldName,
|
||||||
worldKey,
|
worldKey,
|
||||||
dimension,
|
dimension,
|
||||||
seed)
|
seed)
|
||||||
@@ -176,6 +181,7 @@ public final class BukkitWorldReconciler {
|
|||||||
|
|
||||||
private CompletableFuture<LoadResult> loadConfiguredWorld(
|
private CompletableFuture<LoadResult> loadConfiguredWorld(
|
||||||
File configurationFile,
|
File configurationFile,
|
||||||
|
String configuredWorldName,
|
||||||
NamespacedKey worldKey,
|
NamespacedKey worldKey,
|
||||||
String dimension,
|
String dimension,
|
||||||
Long seed
|
Long seed
|
||||||
@@ -187,7 +193,7 @@ public final class BukkitWorldReconciler {
|
|||||||
return CompletableFuture.completedFuture(LoadResult.busy(worldKey, failure));
|
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) {
|
private LifecycleOperationCoordinator.Lease acquireWorldLoad(NamespacedKey worldKey) {
|
||||||
@@ -199,6 +205,7 @@ public final class BukkitWorldReconciler {
|
|||||||
|
|
||||||
private CompletableFuture<LoadResult> loadWithLease(
|
private CompletableFuture<LoadResult> loadWithLease(
|
||||||
File configurationFile,
|
File configurationFile,
|
||||||
|
String configuredWorldName,
|
||||||
NamespacedKey worldKey,
|
NamespacedKey worldKey,
|
||||||
String dimension,
|
String dimension,
|
||||||
Long seed,
|
Long seed,
|
||||||
@@ -214,11 +221,10 @@ public final class BukkitWorldReconciler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BukkitWorldConfiguration.Registration registration;
|
BukkitWorldConfiguration.Registration registration;
|
||||||
String worldName = IrisWorldStorage.logicalName(worldKey);
|
|
||||||
try {
|
try {
|
||||||
registration = BukkitWorldConfiguration.register(
|
registration = BukkitWorldConfiguration.register(
|
||||||
configurationFile,
|
configurationFile,
|
||||||
worldName,
|
configuredWorldName,
|
||||||
dimension,
|
dimension,
|
||||||
seed);
|
seed);
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
@@ -250,7 +256,7 @@ public final class BukkitWorldReconciler {
|
|||||||
try {
|
try {
|
||||||
boolean rolledBack = BukkitWorldConfiguration.removeIfMatching(
|
boolean rolledBack = BukkitWorldConfiguration.removeIfMatching(
|
||||||
configurationFile,
|
configurationFile,
|
||||||
worldName,
|
configuredWorldName,
|
||||||
dimension,
|
dimension,
|
||||||
seed);
|
seed);
|
||||||
return new LoadResult(settled, registration, true, rolledBack, null);
|
return new LoadResult(settled, registration, true, rolledBack, null);
|
||||||
@@ -426,6 +432,10 @@ public final class BukkitWorldReconciler {
|
|||||||
|
|
||||||
Long configuredSeed(String worldName);
|
Long configuredSeed(String worldName);
|
||||||
|
|
||||||
|
String configuredWorldName(NamespacedKey worldKey);
|
||||||
|
|
||||||
|
NamespacedKey worldKeyFromConfiguration(String configuredWorldName);
|
||||||
|
|
||||||
Optional<World> loadedWorld(NamespacedKey worldKey);
|
Optional<World> loadedWorld(NamespacedKey worldKey);
|
||||||
|
|
||||||
CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed);
|
CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed);
|
||||||
@@ -667,6 +677,19 @@ public final class BukkitWorldReconciler {
|
|||||||
return IrisWorlds.readBukkitWorldSeed(worldName);
|
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
|
@Override
|
||||||
public Optional<World> loadedWorld(NamespacedKey worldKey) {
|
public Optional<World> loadedWorld(NamespacedKey worldKey) {
|
||||||
return WorldIdentity.resolve(worldKey);
|
return WorldIdentity.resolve(worldKey);
|
||||||
|
|||||||
+7
-2
@@ -165,7 +165,8 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
|
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;
|
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
@@ -182,6 +183,10 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
return dimension;
|
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
|
* Resolves the biome provider for a world, falling back to the supplied Bukkit default when
|
||||||
* Iris has nothing staged.
|
* Iris has nothing staged.
|
||||||
@@ -212,7 +217,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
if (dim == null) {
|
if (dim == null) {
|
||||||
throw new RuntimeException("Can't find dimension " + id + "!");
|
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 snapshotRoot = IrisWorldStorage.packRoot(worldKey);
|
||||||
File dimensionPackRoot = dim.getLoader().getDataFolder();
|
File dimensionPackRoot = dim.getLoader().getDataFolder();
|
||||||
String packName = dimensionPackRoot.getName();
|
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;
|
||||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
|
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
|
||||||
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction;
|
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.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
@@ -33,7 +34,6 @@ import org.bukkit.event.world.WorldLoadEvent;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -56,19 +56,10 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public NamespacedKey resolveRequestedWorldKey(String requestedName) {
|
public NamespacedKey resolveRequestedWorldKey(String requestedName) {
|
||||||
String requested = Objects.requireNonNull(requestedName, "requestedName").trim();
|
NamespacedKey worldKey = IrisWorldStorage.replacementKeyFromName(
|
||||||
if (requested.isEmpty()) {
|
requestedName,
|
||||||
throw new IllegalArgumentException("World name cannot be empty.");
|
IrisWorldStorage.levelRoot().getName()
|
||||||
}
|
);
|
||||||
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);
|
|
||||||
}
|
|
||||||
ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), toWorldSlotKey(worldKey));
|
ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), toWorldSlotKey(worldKey));
|
||||||
return worldKey;
|
return worldKey;
|
||||||
}
|
}
|
||||||
@@ -76,14 +67,13 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
public synchronized StagedReplacement stageReplacement(
|
public synchronized StagedReplacement stageReplacement(
|
||||||
VolmitSender sender,
|
VolmitSender sender,
|
||||||
NamespacedKey worldKey,
|
NamespacedKey worldKey,
|
||||||
IrisDimension dimension,
|
IrisDimension dimension
|
||||||
long seed
|
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
VolmitSender requiredSender = Objects.requireNonNull(sender, "sender");
|
VolmitSender requiredSender = Objects.requireNonNull(sender, "sender");
|
||||||
NamespacedKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey");
|
NamespacedKey requiredWorldKey = Objects.requireNonNull(worldKey, "worldKey");
|
||||||
WorldSlotKey requiredWorldSlotKey = toWorldSlotKey(requiredWorldKey);
|
WorldSlotKey requiredWorldSlotKey = toWorldSlotKey(requiredWorldKey);
|
||||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
IrisStartupValidation.requireWorldCreationReady();
|
IrisStartupValidation.requireWorldReplacementStagingReady();
|
||||||
if (!WorldReplacementBootstrapMarker.wasBootstrappedThisProcess()) {
|
if (!WorldReplacementBootstrapMarker.wasBootstrappedThisProcess()) {
|
||||||
throw new IOException("Exact world replacement requires a full Paper-family startup bootstrap.");
|
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) {
|
if (findTransaction(requiredWorldSlotKey) != null) {
|
||||||
throw new IOException("A replacement is already pending for " + requiredWorldKey + ".");
|
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());
|
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);
|
String worldName = WorldReplacementJournal.logicalWorldName(target.levelRoot(), requiredWorldSlotKey);
|
||||||
DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true);
|
DatapackInstallResult datapacks = ServerConfigurator.installDataPacksIfChanged(true);
|
||||||
if (!datapacks.succeeded()) {
|
if (!datapacks.succeeded()) {
|
||||||
throw new IOException("Iris could not compile the dimension datapacks.");
|
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;
|
boolean targetPresent = true;
|
||||||
WorldGeneratorSnapshot originalConfiguration = BukkitWorldConfiguration.snapshot(
|
WorldGeneratorSnapshot originalConfiguration = BukkitWorldConfiguration.snapshot(
|
||||||
ServerProperties.BUKKIT_YML,
|
ServerProperties.BUKKIT_YML,
|
||||||
@@ -435,23 +426,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExactWorldSlotPathPolicy.Target prepareTarget(WorldSlotKey worldKey) throws IOException {
|
private static ExactWorldSlotPathPolicy.Target resolveTarget(WorldSlotKey worldKey) {
|
||||||
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
return ExactWorldSlotPathPolicy.resolve(IrisWorldStorage.levelRoot().toPath(), worldKey);
|
||||||
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 Transaction findTransaction(NamespacedKey worldKey) throws IOException {
|
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) {
|
IrisEnvironment expected = switch (slotKind) {
|
||||||
case VANILLA_OVERWORLD -> IrisEnvironment.NORMAL;
|
case VANILLA_OVERWORLD -> IrisEnvironment.NORMAL;
|
||||||
case VANILLA_NETHER -> IrisEnvironment.NETHER;
|
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
|
* Captures vanilla-slot availability on the main thread at startup so staging never blocks
|
||||||
* reads this snapshot so staging never blocks on a main-thread hop while holding the
|
* on a main-thread hop while holding the manager monitor. The context is stable for the
|
||||||
* manager monitor — that inversion froze the server for the full 30s hop timeout whenever
|
* process lifetime because slot replacements only commit across a restart.
|
||||||
* another world loaded during staging. The context is stable for the process lifetime:
|
|
||||||
* slot replacements only commit across a restart.
|
|
||||||
*/
|
*/
|
||||||
public void captureVanillaLevelContext() {
|
public void captureVanillaLevelContext() {
|
||||||
try {
|
try {
|
||||||
vanillaLevelContext = new VanillaLevelContext(
|
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().getAllowNether(),
|
||||||
Iris.instance.getServer().getAllowEnd()
|
Iris.instance.getServer().getAllowEnd()
|
||||||
);
|
);
|
||||||
} catch (Throwable failure) {
|
} 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 {
|
private static void requireVanillaSlotEnabled(SlotKind slotKind) throws IOException {
|
||||||
if (slotKind == SlotKind.IRIS_MANAGED) {
|
if (slotKind != SlotKind.VANILLA_NETHER && slotKind != SlotKind.VANILLA_END) {
|
||||||
return requestedSeed;
|
return;
|
||||||
}
|
}
|
||||||
VanillaLevelContext context = vanillaLevelContext;
|
VanillaLevelContext context = vanillaLevelContext;
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
context = resolveVanillaLevelContext();
|
context = resolveVanillaLevelContext();
|
||||||
vanillaLevelContext = context;
|
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.");
|
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.");
|
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 {
|
private static VanillaLevelContext resolveVanillaLevelContext() throws IOException {
|
||||||
CompletableFuture<VanillaLevelContext> contextFuture = J.sfut(() -> new VanillaLevelContext(
|
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().getAllowNether(),
|
||||||
Iris.instance.getServer().getAllowEnd()
|
Iris.instance.getServer().getAllowEnd()
|
||||||
));
|
));
|
||||||
if (contextFuture == null) {
|
if (contextFuture == null) {
|
||||||
throw new IOException("Could not schedule primary level-seed resolution.");
|
throw new IOException("Could not schedule vanilla-slot availability resolution.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return contextFuture.get(30L, TimeUnit.SECONDS);
|
return contextFuture.get(30L, TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException failure) {
|
} catch (InterruptedException failure) {
|
||||||
Thread.currentThread().interrupt();
|
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) {
|
} 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 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 {
|
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.LifecycleOperationCoordinator;
|
||||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
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.AtomicDirectoryPublisher;
|
||||||
import art.arcane.iris.core.pack.PackDownloader;
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
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.volmlib.util.director.exceptions.DirectorParsingException;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
|
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
|
||||||
import art.arcane.iris.util.common.format.C;
|
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.plugin.VolmitSender;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
@@ -62,28 +62,17 @@ import org.bukkit.World;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
||||||
import static org.bukkit.Bukkit.getServer;
|
import static org.bukkit.Bukkit.getServer;
|
||||||
@@ -109,7 +98,6 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
private CommandPack pack;
|
private CommandPack pack;
|
||||||
private CommandFind find;
|
private CommandFind find;
|
||||||
private CommandDatapack datapack;
|
private CommandDatapack datapack;
|
||||||
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
|
|
||||||
VolmitSender sender = Iris.getSender();
|
VolmitSender sender = Iris.getSender();
|
||||||
|
|
||||||
@Director(description = "Create a new world", descriptionKey = "iris.director.commandiris.director.create_new_world", aliases = {"c"})
|
@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,
|
String type,
|
||||||
@Param(description = "The seed to generate the world with", descriptionKey = "iris.director.commandiris.param.seed_generate_world_with", defaultValue = "1337")
|
@Param(description = "The seed to generate the world with", descriptionKey = "iris.director.commandiris.param.seed_generate_world_with", defaultValue = "1337")
|
||||||
long seed,
|
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
|
|
||||||
) {
|
) {
|
||||||
NamespacedKey worldKey;
|
NamespacedKey worldKey;
|
||||||
try {
|
try {
|
||||||
if (overwrite) {
|
worldKey = IrisWorldStorage.managedKeyFromName(name);
|
||||||
worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(name);
|
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
||||||
} else {
|
|
||||||
worldKey = IrisWorldStorage.managedKeyFromName(name);
|
|
||||||
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
sender().sendMessage(C.RED + e.getMessage());
|
sender().sendMessage(C.RED + e.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String worldName = IrisWorldStorage.logicalName(worldKey);
|
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")) {
|
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_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS));
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD));
|
||||||
@@ -159,7 +135,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!overwrite && IrisWorldStorage.dimensionRoot(worldName).exists()) {
|
if (IrisWorldStorage.dimensionRoot(worldName).exists()) {
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -177,31 +153,13 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
return;
|
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);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (J.isFolia()) {
|
if (J.isFolia()) {
|
||||||
if (stageFoliaWorldCreation(worldName, dimension, seed, main)) {
|
boolean staged = stageFoliaWorldCreation(worldName, dimension, seed);
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTART_SERVER_GENERATE_LOAD, MessageArgument.untrusted("worldName", worldName)));
|
if (!staged) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,12 +171,6 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
.sender(sender())
|
.sender(sender())
|
||||||
.studio(false)
|
.studio(false)
|
||||||
.create();
|
.create();
|
||||||
if (main) {
|
|
||||||
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
|
|
||||||
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
|
|
||||||
return new Thread(() -> updateMainWorld(worldName));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (reportExpectedCreationInterruption(e)) {
|
if (reportExpectedCreationInterruption(e)) {
|
||||||
return;
|
return;
|
||||||
@@ -229,207 +181,65 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD));
|
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) {
|
@Director(
|
||||||
LifecycleOperationCoordinator.Lease lease;
|
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 {
|
try {
|
||||||
lease = LifecycleOperationCoordinator.get().acquire(
|
worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(target);
|
||||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
} catch (IllegalArgumentException e) {
|
||||||
LifecycleOperationCoordinator.OperationKind.WORLD_PROMOTE,
|
sender().sendMessage(C.RED + e.getMessage());
|
||||||
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);
|
|
||||||
return;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeServerPropertiesAtomically(Path propertiesFile, Properties data) throws IOException {
|
String resolvedType = type.equalsIgnoreCase("default")
|
||||||
Path target = propertiesFile.toAbsolutePath().normalize();
|
? IrisSettings.get().getGenerator().getDefaultWorldType()
|
||||||
Path parent = target.getParent();
|
: type;
|
||||||
if (parent == null) {
|
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
|
||||||
throw new IOException("server.properties has no parent directory.");
|
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;
|
||||||
}
|
}
|
||||||
Path stage = Files.createTempFile(parent, ".server.properties.promoting-", ".tmp");
|
|
||||||
IOException operationFailure = null;
|
|
||||||
try {
|
try {
|
||||||
try (FileOutputStream out = new FileOutputStream(stage.toFile())) {
|
PendingWorldReplacementManager.StagedReplacement staged = Iris.instance
|
||||||
data.store(out, null);
|
.pendingWorldReplacements()
|
||||||
out.getFD().sync();
|
.stageReplacement(sender(), worldKey, dimension);
|
||||||
}
|
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||||
try {
|
+ ". Restart once to publish it. The current dimension is retained until Iris verifies the replacement.");
|
||||||
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
} catch (Throwable failure) {
|
||||||
} catch (AtomicMoveNotSupportedException e) {
|
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||||
Files.move(stage, target, StandardCopyOption.REPLACE_EXISTING);
|
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||||
}
|
? failure.getClass().getSimpleName()
|
||||||
} catch (IOException e) {
|
: failure.getMessage();
|
||||||
operationFailure = e;
|
sender().sendMessage(C.RED + "Could not stage the world replacement: " + detail);
|
||||||
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 {
|
try {
|
||||||
IrisStartupValidation.requireWorldCreationReady();
|
IrisStartupValidation.requireWorldCreationReady();
|
||||||
PackValidationRegistry.requireLoadable(
|
PackValidationRegistry.requireLoadable(
|
||||||
@@ -441,6 +251,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(name);
|
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(name);
|
||||||
LifecycleOperationCoordinator.Lease worldLease = null;
|
LifecycleOperationCoordinator.Lease worldLease = null;
|
||||||
File worldFolder = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
File worldFolder = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
||||||
|
Path stagedWorld = null;
|
||||||
try {
|
try {
|
||||||
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||||
worldLease = coordinator.acquire(
|
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.");
|
sender().sendMessage(C.RED + "Failed to compile the Iris datapack. No world files were staged.");
|
||||||
return false;
|
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) {
|
if (installed == null) {
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||||
deleteDirectorySafely(worldFolder);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
|
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publishAbsent(
|
||||||
deleteDirectorySafely(worldFolder);
|
stagedWorld,
|
||||||
return false;
|
targetWorld
|
||||||
}
|
)) {
|
||||||
|
stagedWorld = null;
|
||||||
if (main) {
|
if (!registerWorldInBukkitYml(worldKey, dimension.getLoadKey(), seed)) {
|
||||||
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);
|
|
||||||
return false;
|
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)));
|
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;
|
return true;
|
||||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||||
sender().sendMessage(C.YELLOW + e.getMessage());
|
sender().sendMessage(C.YELLOW + e.getMessage());
|
||||||
return false;
|
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 {
|
} finally {
|
||||||
|
if (stagedWorld != null) {
|
||||||
|
deleteDirectorySafely(stagedWorld.toFile());
|
||||||
|
}
|
||||||
if (worldLease != null) {
|
if (worldLease != null) {
|
||||||
worldLease.close();
|
worldLease.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
|
private boolean registerWorldInBukkitYml(NamespacedKey worldKey, String dimension, Long seed) {
|
||||||
String logicalWorldName = IrisWorldStorage.logicalName(IrisWorldStorage.keyFromName(worldName));
|
String configuredWorldName = IrisWorldStorage.configuredWorldName(
|
||||||
|
worldKey,
|
||||||
|
IrisWorldStorage.levelRoot().getName()
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
BukkitWorldConfiguration.register(BUKKIT_YML, logicalWorldName, dimension, seed);
|
BukkitWorldConfiguration.register(BUKKIT_YML, configuredWorldName, dimension, seed);
|
||||||
Iris.info("Registered \"" + logicalWorldName + "\" in bukkit.yml");
|
Iris.info("Registered \"" + configuredWorldName + "\" in bukkit.yml");
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
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;
|
return configuredSeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String configuredWorldName(NamespacedKey requestedWorldKey) {
|
||||||
|
return requestedWorldKey.getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NamespacedKey worldKeyFromConfiguration(String configuredWorldName) {
|
||||||
|
return worldKey;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<World> loadedWorld(NamespacedKey requestedWorldKey) {
|
public Optional<World> loadedWorld(NamespacedKey requestedWorldKey) {
|
||||||
return worldKey.equals(requestedWorldKey) ? loaded : Optional.empty();
|
return worldKey.equals(requestedWorldKey) ? loaded : Optional.empty();
|
||||||
|
|||||||
+13
@@ -7,6 +7,7 @@ import org.junit.After;
|
|||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -63,6 +64,18 @@ public class IrisWorldGeneratorResolverTest {
|
|||||||
assertFalse(invalid.getBlockingErrors().toString(), invalid.isLoadable());
|
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 {
|
private static void writeValidPack(Path packRoot) throws Exception {
|
||||||
Files.createDirectories(packRoot.resolve("dimensions"));
|
Files.createDirectories(packRoot.resolve("dimensions"));
|
||||||
Files.createDirectories(packRoot.resolve("regions"));
|
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;
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -8,28 +9,42 @@ import java.lang.reflect.Parameter;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class CommandIrisCreateOverwriteContractTest {
|
public class CommandIrisCreateOverwriteContractTest {
|
||||||
@Test
|
@Test
|
||||||
public void createExposesOptInRestartReplacementFlag() throws Exception {
|
public void createOnlyAcceptsNameTypeAndSeed() throws Exception {
|
||||||
Method command = CommandIris.class.getDeclaredMethod(
|
Method command = CommandIris.class.getDeclaredMethod(
|
||||||
"create",
|
"create",
|
||||||
String.class,
|
String.class,
|
||||||
String.class,
|
String.class,
|
||||||
long.class,
|
long.class
|
||||||
boolean.class,
|
|
||||||
boolean.class
|
|
||||||
);
|
);
|
||||||
Parameter overwriteParameter = command.getParameters()[4];
|
|
||||||
Param overwrite = overwriteParameter.getAnnotation(Param.class);
|
|
||||||
|
|
||||||
assertEquals("overwrite", overwrite.name());
|
assertEquals(3, command.getParameterCount());
|
||||||
assertEquals("false", overwrite.defaultValue());
|
assertFalse(Arrays.stream(command.getParameterTypes()).anyMatch(type -> type == boolean.class));
|
||||||
assertTrue(Arrays.asList(overwrite.aliases()).contains("force"));
|
}
|
||||||
|
|
||||||
|
@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(
|
assertEquals(
|
||||||
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
|
"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 {
|
tasks.withType(org.gradle.api.tasks.testing.Test).configureEach {
|
||||||
systemProperty('iris.nmsChunkGeneratorSource',
|
systemProperty('iris.nmsChunkGeneratorSource',
|
||||||
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/IrisChunkGenerator.java").absolutePath)
|
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',
|
systemProperty('iris.nativeStructurePostProcessorSource',
|
||||||
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath)
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath)
|
||||||
systemProperty('iris.nativeStructureStartInjectorSource',
|
systemProperty('iris.nativeStructureStartInjectorSource',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.core;
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
import art.arcane.iris.BuildConstants;
|
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.IrisData;
|
||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
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.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.SimpleFileVisitor;
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -37,9 +39,29 @@ import java.util.concurrent.atomic.AtomicIntegerArray;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class IrisDatapackCompiler {
|
public final class IrisDatapackCompiler {
|
||||||
private static final int INPUT_FINGERPRINT_SCHEMA = 1;
|
private static final int INPUT_FINGERPRINT_SCHEMA = 2;
|
||||||
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
||||||
private static final List<String> INPUT_DIRECTORIES = List.of("dimensions", "biomes", "snippet");
|
private static final List<String> INPUT_DIRECTORIES = List.of("dimensions", "biomes", "snippet");
|
||||||
|
private static final String FLAT_VOID_LEVEL_STEM = """
|
||||||
|
{
|
||||||
|
"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() {
|
private IrisDatapackCompiler() {
|
||||||
}
|
}
|
||||||
@@ -65,28 +87,37 @@ public final class IrisDatapackCompiler {
|
|||||||
|
|
||||||
public static String computeInputFingerprint(
|
public static String computeInputFingerprint(
|
||||||
List<File> packRoots,
|
List<File> packRoots,
|
||||||
|
List<IrisGeneratorBinding> bindings,
|
||||||
IDataFixer fixer,
|
IDataFixer fixer,
|
||||||
boolean adjustVanillaHeight
|
boolean adjustVanillaHeight
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
Objects.requireNonNull(fixer, "fixer");
|
Objects.requireNonNull(fixer, "fixer");
|
||||||
return computeInputFingerprint(
|
return computeInputFingerprint(
|
||||||
packRoots,
|
packRoots,
|
||||||
|
bindings,
|
||||||
adjustVanillaHeight,
|
adjustVanillaHeight,
|
||||||
compilerIdentity(fixer));
|
compilerIdentity(fixer));
|
||||||
}
|
}
|
||||||
|
|
||||||
static String computeInputFingerprint(
|
static String computeInputFingerprint(
|
||||||
List<File> packRoots,
|
List<File> packRoots,
|
||||||
|
List<IrisGeneratorBinding> bindings,
|
||||||
boolean adjustVanillaHeight,
|
boolean adjustVanillaHeight,
|
||||||
String compilerIdentity
|
String compilerIdentity
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
Objects.requireNonNull(packRoots, "packRoots");
|
Objects.requireNonNull(packRoots, "packRoots");
|
||||||
Objects.requireNonNull(compilerIdentity, "compilerIdentity");
|
Objects.requireNonNull(compilerIdentity, "compilerIdentity");
|
||||||
|
List<IrisGeneratorBinding> normalizedBindings = normalizeBindings(bindings);
|
||||||
MessageDigest digest = sha256();
|
MessageDigest digest = sha256();
|
||||||
updateDigestString(digest, "iris-datapack-compiler-input");
|
updateDigestString(digest, "iris-datapack-compiler-input");
|
||||||
updateDigestInt(digest, INPUT_FINGERPRINT_SCHEMA);
|
updateDigestInt(digest, INPUT_FINGERPRINT_SCHEMA);
|
||||||
updateDigestString(digest, compilerIdentity);
|
updateDigestString(digest, compilerIdentity);
|
||||||
digest.update((byte) (adjustVanillaHeight ? 1 : 0));
|
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());
|
updateDigestInt(digest, packRoots.size());
|
||||||
|
|
||||||
for (int index = 0; index < packRoots.size(); index++) {
|
for (int index = 0; index < packRoots.size(); index++) {
|
||||||
@@ -139,12 +170,14 @@ public final class IrisDatapackCompiler {
|
|||||||
public static CompilationResult compile(
|
public static CompilationResult compile(
|
||||||
List<File> packRoots,
|
List<File> packRoots,
|
||||||
KList<File> datapackRoots,
|
KList<File> datapackRoots,
|
||||||
|
List<IrisGeneratorBinding> bindings,
|
||||||
IDataFixer fixer,
|
IDataFixer fixer,
|
||||||
boolean adjustVanillaHeight
|
boolean adjustVanillaHeight
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
Objects.requireNonNull(packRoots, "packRoots");
|
Objects.requireNonNull(packRoots, "packRoots");
|
||||||
Objects.requireNonNull(datapackRoots, "datapackRoots");
|
Objects.requireNonNull(datapackRoots, "datapackRoots");
|
||||||
Objects.requireNonNull(fixer, "fixer");
|
Objects.requireNonNull(fixer, "fixer");
|
||||||
|
List<IrisGeneratorBinding> normalizedBindings = normalizeBindings(bindings);
|
||||||
if (datapackRoots.isEmpty()) {
|
if (datapackRoots.isEmpty()) {
|
||||||
throw new IOException("No Iris datapack output roots were provided");
|
throw new IOException("No Iris datapack output roots were provided");
|
||||||
}
|
}
|
||||||
@@ -154,6 +187,7 @@ public final class IrisDatapackCompiler {
|
|||||||
|
|
||||||
DimensionHeight height = new DimensionHeight(fixer);
|
DimensionHeight height = new DimensionHeight(fixer);
|
||||||
Map<String, KSet<String>> biomes = new LinkedHashMap<>();
|
Map<String, KSet<String>> biomes = new LinkedHashMap<>();
|
||||||
|
Map<String, List<DimensionCandidate>> dimensions = new LinkedHashMap<>();
|
||||||
int packCount = 0;
|
int packCount = 0;
|
||||||
int dimensionCount = 0;
|
int dimensionCount = 0;
|
||||||
for (File packRoot : packRoots) {
|
for (File packRoot : packRoots) {
|
||||||
@@ -177,6 +211,12 @@ public final class IrisDatapackCompiler {
|
|||||||
}
|
}
|
||||||
IrisLogging.debug(" Compiling Dimension " + dimension.getLoadFile().getPath());
|
IrisLogging.debug(" Compiling Dimension " + dimension.getLoadFile().getPath());
|
||||||
height.merge(dimension);
|
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<>());
|
KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
|
||||||
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes);
|
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes);
|
||||||
dimension.installDimensionType(fixer, datapackRoots);
|
dimension.installDimensionType(fixer, datapackRoots);
|
||||||
@@ -192,10 +232,80 @@ public final class IrisDatapackCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
|
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
|
||||||
validateOutputs(datapackRoots, dimensionCount);
|
installLevelStemBindings(normalizedBindings, dimensions, datapackRoots);
|
||||||
|
validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size());
|
||||||
return new CompilationResult(packCount, dimensionCount, countBiomes(biomes));
|
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(
|
private static void collectInstalledPackRoots(
|
||||||
Path packsRoot,
|
Path packsRoot,
|
||||||
Map<Path, File> roots,
|
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) {
|
for (File datapackRoot : datapackRoots) {
|
||||||
Path root = datapackRoot.toPath();
|
Path root = datapackRoot.toPath();
|
||||||
if (!Files.isRegularFile(root.resolve("pack.mcmeta"))) {
|
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"))) {
|
if (dimensionCount > 0 && !Files.isDirectory(root.resolve("data/iris/dimension_type"))) {
|
||||||
throw new IOException("Iris dimension types were not generated at " + root);
|
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 CompilerInputEntry(Path source, String relativePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record DimensionCandidate(
|
||||||
|
IrisDimension dimension,
|
||||||
|
Path packRoot,
|
||||||
|
String dimensionTypeJson
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public static final class DimensionHeight {
|
public static final class DimensionHeight {
|
||||||
private final IDataFixer fixer;
|
private final IDataFixer fixer;
|
||||||
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
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() {
|
static Snapshot snapshot() {
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import java.io.File;
|
|||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -125,6 +126,54 @@ public final class IrisWorldStorage {
|
|||||||
return key;
|
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) {
|
static NamespacedKey keyFromName(String worldName, String levelName) {
|
||||||
String name = Objects.requireNonNull(worldName, "worldName").trim();
|
String name = Objects.requireNonNull(worldName, "worldName").trim();
|
||||||
String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim();
|
String mainLevelName = Objects.requireNonNull(levelName, "levelName").trim();
|
||||||
@@ -145,6 +194,53 @@ public final class IrisWorldStorage {
|
|||||||
return new NamespacedKey(IRIS_NAMESPACE, key);
|
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) {
|
public static String logicalName(WorldInfo world) {
|
||||||
return logicalName(WorldIdentity.key(world));
|
return logicalName(WorldIdentity.key(world));
|
||||||
}
|
}
|
||||||
@@ -214,6 +310,15 @@ public final class IrisWorldStorage {
|
|||||||
return target.toFile();
|
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) {
|
public static File dimensionRoot(File levelRoot, NamespacedKey key) {
|
||||||
Path dimensionsRoot = Objects.requireNonNull(levelRoot, "levelRoot")
|
Path dimensionsRoot = Objects.requireNonNull(levelRoot, "levelRoot")
|
||||||
.toPath()
|
.toPath()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import art.arcane.volmlib.util.io.IO;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
@@ -25,9 +26,11 @@ import java.nio.channels.FileChannel;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@@ -35,34 +38,41 @@ public class IrisWorlds {
|
|||||||
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
|
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType();
|
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 final KMap<String, String> worlds;
|
||||||
private volatile boolean dirty = false;
|
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<>();
|
this.worlds = new KMap<>();
|
||||||
worlds.forEach((identity, type) -> this.worlds.put(WorldIdentity.parse(identity).toString(), type));
|
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();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IrisWorlds get() {
|
public static IrisWorlds get() {
|
||||||
return cache.aquire(() -> {
|
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()) {
|
if (!file.exists()) {
|
||||||
return new IrisWorlds(new KMap<>());
|
return new IrisWorlds(levelRoot, new KMap<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String json = IO.readAll(file);
|
String json = IO.readAll(file);
|
||||||
KMap<String, String> worlds = GSON.fromJson(json, TYPE);
|
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) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.error("Failed to load worlds.json!");
|
IrisLogging.error("Failed to load worlds.json for level root " + levelRoot + "!");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
IrisLogging.reportError(e);
|
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() {
|
public synchronized KMap<String, String> getWorlds() {
|
||||||
clean();
|
clean();
|
||||||
KMap<String, String> result = new KMap<>();
|
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);
|
return result.put(worlds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +147,7 @@ public class IrisWorlds {
|
|||||||
public synchronized void clean() {
|
public synchronized void clean() {
|
||||||
boolean removed = worlds.entrySet().removeIf(entry -> {
|
boolean removed = worlds.entrySet().removeIf(entry -> {
|
||||||
try {
|
try {
|
||||||
File packRoot = IrisWorldStorage.packRoot(WorldIdentity.parse(entry.getKey()));
|
File packRoot = packRoot(entry.getKey());
|
||||||
return !new File(packRoot, "dimensions/" + entry.getValue() + ".json").exists();
|
return !new File(packRoot, "dimensions/" + entry.getValue() + ".json").exists();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return true;
|
return true;
|
||||||
@@ -159,7 +171,7 @@ public class IrisWorlds {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path target = IrisPlatforms.get().dataFile("worlds.json").toPath().toAbsolutePath().normalize();
|
Path target = registryFile;
|
||||||
Path parent = target.getParent();
|
Path parent = target.getParent();
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
throw new IOException("worlds.json target has no parent: " + target);
|
throw new IOException("worlds.json target has no parent: " + target);
|
||||||
@@ -193,6 +205,49 @@ public class IrisWorlds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static KMap<String, String> readBukkitWorlds() {
|
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);
|
YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||||
ConfigurationSection worlds = bukkit.getConfigurationSection("worlds");
|
ConfigurationSection worlds = bukkit.getConfigurationSection("worlds");
|
||||||
if (worlds == null) return new KMap<>();
|
if (worlds == null) return new KMap<>();
|
||||||
@@ -212,11 +267,16 @@ public class IrisWorlds {
|
|||||||
result.put(world, loadKey);
|
result.put(world, loadKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return filterBukkitWorldsByStorage(levelRoot, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisDimension loadDimension(String worldIdentity, String id) {
|
private File packRoot(String worldIdentity) {
|
||||||
File pack = IrisWorldStorage.packRoot(WorldIdentity.parse(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;
|
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
dimension = IrisData.loadAnyDimension(id, 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.datapack.DatapackIngestService.ReapplyOutcome;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
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.lifecycle.LifecycleOperationCoordinator;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||||
@@ -276,10 +278,12 @@ public class ServerConfigurator {
|
|||||||
IrisLogging.debug("Checking Data Packs...");
|
IrisLogging.debug("Checking Data Packs...");
|
||||||
}
|
}
|
||||||
List<File> packRoots;
|
List<File> packRoots;
|
||||||
|
List<IrisGeneratorBinding> bindings;
|
||||||
try {
|
try {
|
||||||
packRoots = collectCompilerPackRoots();
|
packRoots = collectCompilerPackRoots();
|
||||||
|
bindings = collectConfiguredLevelStemBindings();
|
||||||
} catch (IOException exception) {
|
} 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();
|
return DatapackInstallResult.failedResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +307,7 @@ public class ServerConfigurator {
|
|||||||
IrisDatapackCompiler.compile(
|
IrisDatapackCompiler.compile(
|
||||||
packRoots,
|
packRoots,
|
||||||
stagedRoots,
|
stagedRoots,
|
||||||
|
bindings,
|
||||||
fixer,
|
fixer,
|
||||||
IrisSettings.get().getGeneral().adjustVanillaHeight
|
IrisSettings.get().getGeneral().adjustVanillaHeight
|
||||||
);
|
);
|
||||||
@@ -445,11 +450,25 @@ public class ServerConfigurator {
|
|||||||
IrisWorldStorage.levelRoot().toPath());
|
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 {
|
private static String computeCurrentDatapackCompilerInputFingerprint(IDataFixer fixer) throws IOException {
|
||||||
return IrisDatapackCompiler.computeInputFingerprint(
|
return IrisDatapackCompiler.computeInputFingerprint(
|
||||||
IrisDatapackCompiler.collectCompilerInputRoots(
|
IrisDatapackCompiler.collectCompilerInputRoots(
|
||||||
IrisPlatforms.get().dataFolder().toPath(),
|
IrisPlatforms.get().dataFolder().toPath(),
|
||||||
IrisWorldStorage.levelRoot().toPath()),
|
IrisWorldStorage.levelRoot().toPath()),
|
||||||
|
collectConfiguredLevelStemBindings(),
|
||||||
Objects.requireNonNull(fixer, "Datapack fixer"),
|
Objects.requireNonNull(fixer, "Datapack fixer"),
|
||||||
IrisSettings.get().getGeneral().adjustVanillaHeight);
|
IrisSettings.get().getGeneral().adjustVanillaHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package art.arcane.iris.core.lifecycle;
|
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.ConfigurationSection;
|
||||||
import org.bukkit.configuration.InvalidConfigurationException;
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
@@ -15,6 +18,10 @@ import java.nio.file.StandardCopyOption;
|
|||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.util.ArrayList;
|
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.Objects;
|
||||||
import java.util.function.Predicate;
|
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(
|
public static GeneratorReplacement replaceIfMatching(
|
||||||
File configurationFile,
|
File configurationFile,
|
||||||
String worldName,
|
String worldName,
|
||||||
@@ -396,11 +492,40 @@ public final class BukkitWorldConfiguration {
|
|||||||
return value.trim();
|
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 {
|
public enum Registration {
|
||||||
CREATED,
|
CREATED,
|
||||||
UNCHANGED
|
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(
|
public record WorldGeneratorSnapshot(
|
||||||
boolean worldsSectionPresent,
|
boolean worldsSectionPresent,
|
||||||
boolean worldSectionPresent,
|
boolean worldSectionPresent,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
|||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.entity.Player;
|
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 static final class RemovalFailure extends CompletionException {
|
||||||
private final RemovalStatus status;
|
private final RemovalStatus status;
|
||||||
private final Path quarantineDirectory;
|
private final Path quarantineDirectory;
|
||||||
@@ -840,7 +863,7 @@ public final class IrisWorldRemovalService {
|
|||||||
try {
|
try {
|
||||||
boolean bukkitChanged = BukkitWorldConfiguration.remove(
|
boolean bukkitChanged = BukkitWorldConfiguration.remove(
|
||||||
ServerProperties.BUKKIT_YML,
|
ServerProperties.BUKKIT_YML,
|
||||||
target.logicalName()
|
bukkitConfigurationWorldName(target)
|
||||||
);
|
);
|
||||||
return new ConfigurationDisposition(multiverseChanged || bukkitChanged, null);
|
return new ConfigurationDisposition(multiverseChanged || bukkitChanged, null);
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
@@ -931,7 +954,7 @@ public final class IrisWorldRemovalService {
|
|||||||
);
|
);
|
||||||
boolean directoryPresent = Files.isDirectory(target.worldDirectory(), LinkOption.NOFOLLOW_LINKS);
|
boolean directoryPresent = Files.isDirectory(target.worldDirectory(), LinkOption.NOFOLLOW_LINKS);
|
||||||
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||||
String generator = configuration.getString("worlds." + target.logicalName() + ".generator");
|
String generator = bukkitGenerator(configuration, target);
|
||||||
boolean configurationManaged = generator != null
|
boolean configurationManaged = generator != null
|
||||||
&& (generator.equalsIgnoreCase("Iris") || generator.regionMatches(true, 0, "Iris:", 0, 5));
|
&& (generator.equalsIgnoreCase("Iris") || generator.regionMatches(true, 0, "Iris:", 0, 5));
|
||||||
boolean conflictingConfiguration = generator != null && !configurationManaged;
|
boolean conflictingConfiguration = generator != null && !configurationManaged;
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ public final class LifecycleOperationCoordinator {
|
|||||||
WORLD_UNLOAD,
|
WORLD_UNLOAD,
|
||||||
WORLD_REMOVE,
|
WORLD_REMOVE,
|
||||||
WORLD_REPLACE,
|
WORLD_REPLACE,
|
||||||
WORLD_PROMOTE,
|
|
||||||
STUDIO_OPEN,
|
STUDIO_OPEN,
|
||||||
STUDIO_CLOSE,
|
STUDIO_CLOSE,
|
||||||
PACK_CREATE,
|
PACK_CREATE,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
|||||||
|
|
||||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||||
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
|
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
|
||||||
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -27,6 +28,7 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class WorldReplacementFilesystem {
|
public final class WorldReplacementFilesystem {
|
||||||
|
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
|
||||||
private static final List<Path> PAPER_WORLD_METADATA = List.of(
|
private static final List<Path> PAPER_WORLD_METADATA = List.of(
|
||||||
Path.of("data/paper/metadata.dat"),
|
Path.of("data/paper/metadata.dat"),
|
||||||
Path.of("data/paper/level_overrides.dat"),
|
Path.of("data/paper/level_overrides.dat"),
|
||||||
@@ -55,15 +57,15 @@ public final class WorldReplacementFilesystem {
|
|||||||
ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths");
|
ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths");
|
||||||
State state = inspect(requiredPaths);
|
State state = inspect(requiredPaths);
|
||||||
if (!state.targetPresent()) {
|
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()) {
|
if (state.stagePresent() || state.backupPresent()) {
|
||||||
throw new IOException("A replacement artifact already exists for this transaction.");
|
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 {
|
try {
|
||||||
requireDirectory(retainedWorld, "retained world");
|
requireDirectory(retainedWorld, "retained world");
|
||||||
requireDirectory(retainedWorld.resolve("data"), "retained world data");
|
requireDirectory(retainedWorld.resolve("data"), "retained world data");
|
||||||
@@ -249,13 +251,15 @@ public final class WorldReplacementFilesystem {
|
|||||||
try (Stream<Path> stream = Files.walk(root)) {
|
try (Stream<Path> stream = Files.walk(root)) {
|
||||||
files = stream
|
files = stream
|
||||||
.filter(path -> !path.equals(root))
|
.filter(path -> !path.equals(root))
|
||||||
.filter(path -> !containsMetadataSegment(root.relativize(path)))
|
|
||||||
.sorted(Comparator.comparing(path -> root.relativize(path).toString()))
|
.sorted(Comparator.comparing(path -> root.relativize(path).toString()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
for (Path file : files) {
|
for (Path file : files) {
|
||||||
BasicFileAttributes attributes = requireSafeEntry(file);
|
BasicFileAttributes attributes = requireSafeEntry(file);
|
||||||
Path relative = root.relativize(file);
|
Path relative = root.relativize(file);
|
||||||
|
if (isGeneratedPackMetadata(relative, attributes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
update(digest, relative.toString().replace(file.getFileSystem().getSeparator(), "/"));
|
update(digest, relative.toString().replace(file.getFileSystem().getSeparator(), "/"));
|
||||||
digest.update((byte) (attributes.isDirectory() ? 1 : 0));
|
digest.update((byte) (attributes.isDirectory() ? 1 : 0));
|
||||||
if (!attributes.isRegularFile()) {
|
if (!attributes.isRegularFile()) {
|
||||||
@@ -304,13 +308,10 @@ public final class WorldReplacementFilesystem {
|
|||||||
return fingerprint;
|
return fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean containsMetadataSegment(Path relative) {
|
private static boolean isGeneratedPackMetadata(Path relative, BasicFileAttributes attributes) {
|
||||||
for (Path component : relative) {
|
return PackDirectoryResolver.isHiddenName(relative.getName(0).toString())
|
||||||
if (".iris".equals(component.toString())) {
|
|| attributes.isRegularFile()
|
||||||
return true;
|
&& relative.getFileName().toString().endsWith(CODE_WORKSPACE_SUFFIX);
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BasicFileAttributes requireSafeEntry(Path path) throws IOException {
|
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 {
|
private static void preservePaperWorldMetadata(Path retainedWorld, Path replacementWorld) throws IOException {
|
||||||
requireMigratedRetainedWorld(retainedWorld);
|
requireCurrentPaperWorld(retainedWorld);
|
||||||
requireDirectory(replacementWorld, "replacement world");
|
requireDirectory(replacementWorld, "replacement world");
|
||||||
ensureDirectory(replacementWorld.resolve("data"), "replacement world data");
|
ensureDirectory(replacementWorld.resolve("data"), "replacement world data");
|
||||||
ensureDirectory(replacementWorld.resolve("data/paper"), "replacement Paper data");
|
ensureDirectory(replacementWorld.resolve("data/paper"), "replacement Paper data");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.core.lifecycle;
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||||
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
import art.arcane.iris.core.WorldSlotKey;
|
import art.arcane.iris.core.WorldSlotKey;
|
||||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot;
|
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot;
|
||||||
|
|
||||||
@@ -152,15 +153,7 @@ public final class WorldReplacementJournal {
|
|||||||
}
|
}
|
||||||
String levelName = fileName.toString();
|
String levelName = fileName.toString();
|
||||||
if ("iris".equals(requiredWorldKey.namespace())) {
|
if ("iris".equals(requiredWorldKey.namespace())) {
|
||||||
String logicalName = requiredWorldKey.key();
|
return IrisWorldStorage.configuredWorldName(requiredWorldKey, levelName);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (WorldSlotKey.minecraft("overworld").equals(requiredWorldKey)) {
|
if (WorldSlotKey.minecraft("overworld").equals(requiredWorldKey)) {
|
||||||
return levelName;
|
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",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend",
|
||||||
C.YELLOW + "Try one of: overworld, vanilla, flat, theend"
|
C.YELLOW + "Try one of: overworld, vanilla, flat, theend"
|
||||||
);
|
);
|
||||||
public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTART_SERVER_GENERATE_LOAD = TextKey.of(
|
public static final TextKey COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD = TextKey.of(
|
||||||
"iris.bukkit.commandiris.world_staging_completed_restart_server_generate_load",
|
"iris.bukkit.commandiris.world_staging_completed_restarting_server_generate_load",
|
||||||
C.GREEN + "World staging completed. Restart the server to generate/load \"" + "{worldName}" + "\"."
|
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(
|
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",
|
"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",
|
"iris.bukkit.commandiris.successfully_created_your_world",
|
||||||
C.GREEN + "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(
|
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",
|
"iris.bukkit.commandiris.runtime_world_creation_is_disabled_on_folia",
|
||||||
C.YELLOW + "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",
|
"iris.bukkit.commandiris.failed_stage_world_files_dimension",
|
||||||
C.RED + "Failed to stage world files for dimension \"" + "{value}" + "\"."
|
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(
|
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",
|
"iris.bukkit.commandiris.staged_iris_world_with_generator_iris_seed",
|
||||||
C.GREEN + "Staged Iris world \"" + "{name}" + "\" with generator Iris:" + "{value}" + " and seed " + "{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(
|
public static final TextKey COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML = TextKey.of(
|
||||||
"iris.bukkit.commandiris.failed_update_bukkit_yml",
|
"iris.bukkit.commandiris.failed_update_bukkit_yml",
|
||||||
C.RED + "Failed to update bukkit.yml: " + "{value}"
|
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_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2,
|
||||||
COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS,
|
COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS,
|
||||||
COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND,
|
COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND,
|
||||||
COMMAND_IRIS_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_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS,
|
||||||
COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD,
|
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_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA,
|
||||||
COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP,
|
COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP,
|
||||||
COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION,
|
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_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_FAILED_UPDATE_BUKKIT_YML,
|
||||||
COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST,
|
COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST,
|
||||||
COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE,
|
COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE,
|
||||||
|
|||||||
@@ -282,10 +282,6 @@ public final class DirectorCommandMessages {
|
|||||||
"iris.director.commandiris.param.seed_generate_world_with",
|
"iris.director.commandiris.param.seed_generate_world_with",
|
||||||
"The seed to generate the 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(
|
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",
|
"iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
|
||||||
"Replace the exact existing world slot on the 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_NAME_WORLD_CREATE,
|
||||||
COMMAND_IRIS_PARAM_DIMENSION_PACK_CREATE_WORLD_WITH,
|
COMMAND_IRIS_PARAM_DIMENSION_PACK_CREATE_WORLD_WITH,
|
||||||
COMMAND_IRIS_PARAM_SEED_GENERATE_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_PARAM_REPLACE_EXACT_EXISTING_WORLD_SLOT_NEXT_RESTART,
|
||||||
COMMAND_IRIS_DIRECTOR_TELEPORT_ANOTHER_WORLD,
|
COMMAND_IRIS_DIRECTOR_TELEPORT_ANOTHER_WORLD,
|
||||||
COMMAND_IRIS_PARAM_WORLD_TELEPORT,
|
COMMAND_IRIS_PARAM_WORLD_TELEPORT,
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ import org.bukkit.generator.ChunkGenerator;
|
|||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -269,6 +271,14 @@ public interface INMSBinding {
|
|||||||
default void uninjectBukkit() {
|
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();
|
KMap<Material, List<BlockProperty>> getBlockProperties();
|
||||||
|
|
||||||
private void validateDimensionTypes(WorldCreator c) {
|
private void validateDimensionTypes(WorldCreator c) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.pack;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
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 {
|
private static void move(Path source, Path target) throws IOException {
|
||||||
try {
|
try {
|
||||||
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
|
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.IrisDatapackCompiler;
|
||||||
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
|
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.DataVersion;
|
||||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -45,7 +47,7 @@ import java.util.zip.ZipInputStream;
|
|||||||
public final class DefaultPackBootstrapProvisioner {
|
public final class DefaultPackBootstrapProvisioner {
|
||||||
private static final List<PackSpec> DEFAULT_PACKS = List.of();
|
private static final List<PackSpec> DEFAULT_PACKS = List.of();
|
||||||
private static final String WORLD_DATAPACK_DIRECTORY = "iris";
|
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 int MAX_ARCHIVE_ENTRIES = 100_000;
|
||||||
private static final long MAX_ARCHIVE_BYTES = 512L * 1024L * 1024L;
|
private static final long MAX_ARCHIVE_BYTES = 512L * 1024L * 1024L;
|
||||||
private static final long MAX_EXPANDED_BYTES = 2L * 1024L * 1024L * 1024L;
|
private static final long MAX_EXPANDED_BYTES = 2L * 1024L * 1024L * 1024L;
|
||||||
@@ -71,13 +73,20 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
Objects.requireNonNull(dataDirectory, "dataDirectory");
|
Objects.requireNonNull(dataDirectory, "dataDirectory");
|
||||||
Objects.requireNonNull(feedback, "feedback");
|
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()
|
HttpClient client = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(15))
|
.connectTimeout(Duration.ofSeconds(15))
|
||||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||||
.build();
|
.build();
|
||||||
ProvisionOptions options = new ProvisionOptions(
|
ProvisionOptions options = new ProvisionOptions(
|
||||||
DEFAULT_PACKS,
|
DEFAULT_PACKS,
|
||||||
|
bindings,
|
||||||
client,
|
client,
|
||||||
Clock.systemUTC(),
|
Clock.systemUTC(),
|
||||||
Duration.ofMinutes(30),
|
Duration.ofMinutes(30),
|
||||||
@@ -95,20 +104,24 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return isProvisioned(
|
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
|
||||||
dataDirectory,
|
List<IrisGeneratorBinding> bindings = BukkitWorldConfiguration.readIrisGeneratorBindings(
|
||||||
BukkitStartupPaths.resolveCurrent().levelRoot()
|
startupPaths.bukkitConfiguration().toFile(),
|
||||||
|
levelId(startupPaths.levelRoot()),
|
||||||
|
startupPaths.levelRoot()
|
||||||
);
|
);
|
||||||
|
return isProvisioned(dataDirectory, startupPaths.levelRoot(), DEFAULT_PACKS, bindings);
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isProvisioned(Path dataDirectory, Path levelRoot) {
|
static boolean isProvisioned(
|
||||||
return isProvisioned(dataDirectory, levelRoot, DEFAULT_PACKS);
|
Path dataDirectory,
|
||||||
}
|
Path levelRoot,
|
||||||
|
List<PackSpec> requiredPacks,
|
||||||
static boolean isProvisioned(Path dataDirectory, Path levelRoot, List<PackSpec> requiredPacks) {
|
List<IrisGeneratorBinding> bindings
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
Path normalizedData = dataDirectory.toAbsolutePath().normalize();
|
Path normalizedData = dataDirectory.toAbsolutePath().normalize();
|
||||||
Path normalizedLevel = levelRoot.toAbsolutePath().normalize();
|
Path normalizedLevel = levelRoot.toAbsolutePath().normalize();
|
||||||
@@ -137,12 +150,19 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
List<File> packRoots = IrisDatapackCompiler.collectPackRoots(
|
||||||
|
normalizedData,
|
||||||
|
normalizedLevel
|
||||||
|
);
|
||||||
return directoryFingerprint(datapackRoot).equals(marker.getProperty("datapackFingerprint"))
|
return directoryFingerprint(datapackRoot).equals(marker.getProperty("datapackFingerprint"))
|
||||||
&& datapackRoot.toString().equals(marker.getProperty("datapackPath"))
|
&& datapackRoot.toString().equals(marker.getProperty("datapackPath"))
|
||||||
&& packRootsFingerprint(IrisDatapackCompiler.collectPackRoots(
|
&& packRootsFingerprint(packRoots).equals(marker.getProperty("aggregateFingerprint"))
|
||||||
normalizedData,
|
&& IrisDatapackCompiler.computeInputFingerprint(
|
||||||
normalizedLevel
|
packRoots,
|
||||||
)).equals(marker.getProperty("aggregateFingerprint"));
|
bindings,
|
||||||
|
fixer,
|
||||||
|
false
|
||||||
|
).equals(marker.getProperty("compilerInputFingerprint"));
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -234,10 +254,17 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
}
|
}
|
||||||
String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer);
|
String compilerIdentity = IrisDatapackCompiler.compilerIdentity(fixer);
|
||||||
String aggregateFingerprint = packRootsFingerprint(packRoots);
|
String aggregateFingerprint = packRootsFingerprint(packRoots);
|
||||||
|
String compilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||||
|
packRoots,
|
||||||
|
options.bindings(),
|
||||||
|
fixer,
|
||||||
|
false
|
||||||
|
);
|
||||||
boolean anyPackReplaced = !packPublications.isEmpty();
|
boolean anyPackReplaced = !packPublications.isEmpty();
|
||||||
boolean rebuildDatapack = anyPackReplaced
|
boolean rebuildDatapack = anyPackReplaced
|
||||||
|| !existingDatapack
|
|| !existingDatapack
|
||||||
|| !aggregateFingerprint.equals(previousMarker.getProperty("aggregateFingerprint"))
|
|| !aggregateFingerprint.equals(previousMarker.getProperty("aggregateFingerprint"))
|
||||||
|
|| !compilerInputFingerprint.equals(previousMarker.getProperty("compilerInputFingerprint"))
|
||||||
|| !compilerIdentity.equals(previousMarker.getProperty("compilerIdentity"))
|
|| !compilerIdentity.equals(previousMarker.getProperty("compilerIdentity"))
|
||||||
|| !datapackRoot.toString().equals(previousMarker.getProperty("datapackPath"))
|
|| !datapackRoot.toString().equals(previousMarker.getProperty("datapackPath"))
|
||||||
|| !directoryFingerprint(datapackRoot).equals(previousMarker.getProperty("datapackFingerprint"));
|
|| !directoryFingerprint(datapackRoot).equals(previousMarker.getProperty("datapackFingerprint"));
|
||||||
@@ -245,7 +272,7 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
compileContainer = datapacksRoot.resolve("." + WORLD_DATAPACK_DIRECTORY + "-stage-" + UUID.randomUUID());
|
compileContainer = datapacksRoot.resolve("." + WORLD_DATAPACK_DIRECTORY + "-stage-" + UUID.randomUUID());
|
||||||
Files.createDirectories(compileContainer);
|
Files.createDirectories(compileContainer);
|
||||||
KList<File> outputFolders = new KList<File>().qadd(compileContainer.toFile());
|
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())) {
|
if (!isDatapackRoot(compileContainer, !packRoots.isEmpty())) {
|
||||||
throw new IOException("Canonical Iris datapack compiler produced incomplete output at " + compileContainer);
|
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 finalAggregateFingerprint = packRootsFingerprint(finalPackRoots);
|
||||||
String finalCompilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
String finalCompilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||||
finalPackRoots,
|
finalPackRoots,
|
||||||
|
options.bindings(),
|
||||||
fixer,
|
fixer,
|
||||||
false);
|
false);
|
||||||
String finalDatapackFingerprint = directoryFingerprint(datapackRoot);
|
String finalDatapackFingerprint = directoryFingerprint(datapackRoot);
|
||||||
@@ -280,6 +308,7 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
}
|
}
|
||||||
marker.setProperty("aggregateFingerprint", finalAggregateFingerprint);
|
marker.setProperty("aggregateFingerprint", finalAggregateFingerprint);
|
||||||
marker.setProperty("compilerIdentity", compilerIdentity);
|
marker.setProperty("compilerIdentity", compilerIdentity);
|
||||||
|
marker.setProperty("compilerInputFingerprint", finalCompilerInputFingerprint);
|
||||||
marker.setProperty("datapackFingerprint", finalDatapackFingerprint);
|
marker.setProperty("datapackFingerprint", finalDatapackFingerprint);
|
||||||
marker.setProperty("datapackPath", datapackRoot.toString());
|
marker.setProperty("datapackPath", datapackRoot.toString());
|
||||||
marker.setProperty("completedAt", Long.toString(options.clock().millis()));
|
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) {
|
private static void deleteQuietly(Path path, Consumer<String> feedback) {
|
||||||
try {
|
try {
|
||||||
delete(path);
|
delete(path);
|
||||||
@@ -880,6 +917,7 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
|
|
||||||
record ProvisionOptions(
|
record ProvisionOptions(
|
||||||
List<PackSpec> packs,
|
List<PackSpec> packs,
|
||||||
|
List<IrisGeneratorBinding> bindings,
|
||||||
HttpClient client,
|
HttpClient client,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
Duration refreshInterval,
|
Duration refreshInterval,
|
||||||
@@ -891,6 +929,7 @@ public final class DefaultPackBootstrapProvisioner {
|
|||||||
) {
|
) {
|
||||||
ProvisionOptions {
|
ProvisionOptions {
|
||||||
packs = List.copyOf(Objects.requireNonNull(packs, "packs"));
|
packs = List.copyOf(Objects.requireNonNull(packs, "packs"));
|
||||||
|
bindings = List.copyOf(Objects.requireNonNull(bindings, "bindings"));
|
||||||
Objects.requireNonNull(client, "client");
|
Objects.requireNonNull(client, "client");
|
||||||
Objects.requireNonNull(clock, "clock");
|
Objects.requireNonNull(clock, "clock");
|
||||||
Objects.requireNonNull(refreshInterval, "refreshInterval");
|
Objects.requireNonNull(refreshInterval, "refreshInterval");
|
||||||
|
|||||||
@@ -134,8 +134,11 @@ public class IrisCreator {
|
|||||||
private BiConsumer<String, Long> studioTimingConsumer;
|
private BiConsumer<String, Long> studioTimingConsumer;
|
||||||
private DatapackPreparation datapackPreparation = DatapackPreparation.INSTALL_IF_CHANGED;
|
private DatapackPreparation datapackPreparation = DatapackPreparation.INSTALL_IF_CHANGED;
|
||||||
|
|
||||||
public static boolean removeFromBukkitYml(String name) throws IOException {
|
public static boolean removeFromBukkitYml(NamespacedKey worldKey) throws IOException {
|
||||||
return BukkitWorldConfiguration.remove(BUKKIT_YML, name);
|
return BukkitWorldConfiguration.remove(
|
||||||
|
BUKKIT_YML,
|
||||||
|
IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int removeTransientStudioWorldsFromBukkitYml() throws IOException {
|
public static int removeTransientStudioWorldsFromBukkitYml() throws IOException {
|
||||||
@@ -293,7 +296,12 @@ public class IrisCreator {
|
|||||||
reportStudioProgress(0.86D, "create_world");
|
reportStudioProgress(0.86D, "create_world");
|
||||||
|
|
||||||
if (!studio && !benchmark) {
|
if (!studio && !benchmark) {
|
||||||
BukkitWorldConfiguration.register(BUKKIT_YML, name, dimension, seed);
|
BukkitWorldConfiguration.register(
|
||||||
|
BUKKIT_YML,
|
||||||
|
IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName()),
|
||||||
|
dimension,
|
||||||
|
seed
|
||||||
|
);
|
||||||
bukkitRegistered = true;
|
bukkitRegistered = true;
|
||||||
World createdWorld = world;
|
World createdWorld = world;
|
||||||
CompletableFuture<Void> multiverseRegistration = J.sfut(
|
CompletableFuture<Void> multiverseRegistration = J.sfut(
|
||||||
@@ -730,7 +738,10 @@ public class IrisCreator {
|
|||||||
failure.addSuppressed(rollbackFailure);
|
failure.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
BukkitWorldConfiguration.remove(BUKKIT_YML, name);
|
BukkitWorldConfiguration.remove(
|
||||||
|
BUKKIT_YML,
|
||||||
|
IrisWorldStorage.configuredWorldName(worldKey, IrisWorldStorage.levelRoot().getName())
|
||||||
|
);
|
||||||
} catch (Throwable rollbackFailure) {
|
} catch (Throwable rollbackFailure) {
|
||||||
failure.addSuppressed(rollbackFailure);
|
failure.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ public class IrisToolbelt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean removeWorld(World world) throws IOException {
|
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) {
|
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.may_we_suggest_name_irisworld_instead_2": "§cWie wäre es stattdessen mit dem Namen \"IrisWorld\"?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cDieser Ordner existiert bereits!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cDieser Ordner existiert bereits!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eVersuche eine der folgenden Optionen: overworld, vanilla, flat, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eVersuche eine der folgenden Optionen: overworld, vanilla, flat, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cBei der Erstellung ist eine Ausnahme aufgetreten. Weitere Details findest du in der Konsole.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aDeine Welt wurde erfolgreich erstellt!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aDeine Welt wurde erfolgreich erstellt!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cDer angegebene Spieler existiert nicht.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} von Volmit Software",
|
"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.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.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.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.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.director.teleport_another_world": "Teleportieren in eine andere Welt",
|
||||||
"iris.director.commandiris.param.world_teleport": "Zielwelt der Teleportation",
|
"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.may_we_suggest_name_irisworld_instead_2": "§c¿Podemos sugerir el nombre \"IrisWorld\"?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c¡Esa carpeta ya existe!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§c¡Esa carpeta ya existe!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePrueba una de estas opciones: overworld, vanilla, flat, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePrueba una de estas opciones: overworld, vanilla, flat, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cSe produjo una excepción durante la creación. Consulta la consola para obtener más detalles.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§a¡Tu mundo se creó correctamente!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§a¡Tu mundo se creó correctamente!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cEl jugador especificado no existe.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} por Volmit Software",
|
"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.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.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.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.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.director.teleport_another_world": "Teletransportarse a otro mundo",
|
||||||
"iris.director.commandiris.param.world_teleport": "El mundo al que se teletransportará",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cVoimmeko ehdottaa nimeä \"IrisWorldSen sijaan?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cSe kansio on jo olemassa!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cSe kansio on jo olemassa!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eKokeile yksi: maapallo, vanilla, litteä, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eKokeile yksi: maapallo, vanilla, litteä, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cLuomisen aikana nostettu poikkeus. Katso lisätietoja konsolista.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aOnnistuneesti loin maailmasi!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aOnnistuneesti loin maailmasi!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cMääriteltyä pelaajaa ei ole olemassa.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris{value} mennessä Volmit Software",
|
"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.name_world_create": "Luotavan maailman nimi",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Ulottuvuus / paketti luoda maailma",
|
"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.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.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.director.teleport_another_world": "Teleporttautuminen toiseen maailmaan",
|
||||||
"iris.director.commandiris.param.world_teleport": "Maailman teleportata",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cNous vous suggérons plutôt le nom \"IrisWorld\".",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cCe dossier existe déjà !",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cCe dossier existe déjà !",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eEssayez l'une des options suivantes : overworld, vanilla, flat, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eEssayez l'une des options suivantes : overworld, vanilla, flat, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cUne exception s'est produite pendant la création. Consultez la console pour plus de détails.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aVotre monde a été créé avec succès !",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aVotre monde a été créé avec succès !",
|
||||||
"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.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.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.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.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.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.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",
|
"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.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.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.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.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.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",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cאפשר להציע את השם \"IrisWorld\"במקום?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cהתיקיה הזו כבר קיימת!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cהתיקיה הזו כבר קיימת!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eנסה: Overworld vanilla, שטוח, TheendXTry",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eנסה: Overworld vanilla, שטוח, TheendXTry",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cלמעטים שגדלו במהלך הבריאה. ראו את הקונסולה לפרטים נוספים.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aיצרתם בהצלחה את עולמכם!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aיצרתם בהצלחה את עולמכם!",
|
||||||
"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.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.preparing_world_files_bukkit_yml_next_startup": "§eהכנת קבצים בעולם bukkit.yml עבור הסטארט-אפ הבא...",
|
||||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cנכשלים בקבצי עולם הבמה לממד \"{value}\".",
|
"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.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.failed_update_bukkit_yml": "§cנכשל לעדכן bukkit.yml: {value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cהשחקן שצוין אינו קיים.",
|
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cהשחקן שצוין אינו קיים.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aאיריס v{value} על ידי Volmit Software",
|
"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.name_world_create": "שם העולם ליצור",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "המימד / החבילה ליצירת העולם עם",
|
"iris.director.commandiris.param.dimension_pack_create_world_with": "המימד / החבילה ליצירת העולם עם",
|
||||||
"iris.director.commandiris.param.seed_generate_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.param.replace_exact_existing_world_slot_next_restart": "החלפת משבצת העולם הקיימת המדויקת בהפעלה מחדש הבאה",
|
||||||
"iris.director.commandiris.director.teleport_another_world": "טלפורט לעולם אחר",
|
"iris.director.commandiris.director.teleport_another_world": "טלפורט לעולם אחר",
|
||||||
"iris.director.commandiris.param.world_teleport": "העולם לטלפורט",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cPossiamo suggerire il nome \"IrisWorld\" invece?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cQuella cartella esiste già!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cQuella cartella esiste già!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProva una delle seguenti opzioni: overworld, vanilla, flat, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProva una delle seguenti opzioni: overworld, vanilla, flat, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cSi è verificata un'eccezione durante la creazione. Consulta la console per maggiori dettagli.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aCreato con successo il tuo mondo!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aCreato con successo il tuo mondo!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cIl giocatore specificato non esiste.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} di Volmit Software",
|
"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.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.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.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.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.director.teleport_another_world": "Teletrasporto in un altro mondo",
|
||||||
"iris.director.commandiris.param.world_teleport": "Il mondo verso cui teletrasportarsi",
|
"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.may_we_suggest_name_irisworld_instead_2": "§c代わりに \"IrisWorld\" という名前はいかがですか?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cそのフォルダーはすでに存在します!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cそのフォルダーはすでに存在します!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e次のいずれかを指定してください: overworld、vanilla、flat、theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e次のいずれかを指定してください: overworld、vanilla、flat、theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§c作成中に例外が発生しました。詳細はコンソールを確認してください。",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aワールドを作成しました!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aワールドを作成しました!",
|
||||||
"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.runtime_world_creation_is_disabled_on_folia": "§eFolia では実行中のワールド作成が無効です。",
|
||||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e次回起動用のワールドファイルと bukkit.yml を準備しています...",
|
"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.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.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.failed_update_bukkit_yml": "§cbukkit.yml を更新できませんでした: {value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定されたプレイヤーは存在しません。",
|
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定されたプレイヤーは存在しません。",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v{value} by Volmit Software",
|
"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.name_world_create": "作成するワールドの名前",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "ワールドの作成に使用するディメンションまたはパック",
|
"iris.director.commandiris.param.dimension_pack_create_world_with": "ワールドの作成に使用するディメンションまたはパック",
|
||||||
"iris.director.commandiris.param.seed_generate_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.param.replace_exact_existing_world_slot_next_restart": "次回の再起動時に既存の正確なワールドスロットを置き換える",
|
||||||
"iris.director.commandiris.director.teleport_another_world": "別のワールドへテレポートします",
|
"iris.director.commandiris.director.teleport_another_world": "別のワールドへテレポートします",
|
||||||
"iris.director.commandiris.param.world_teleport": "テレポート先のワールド",
|
"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.may_we_suggest_name_irisworld_instead_2": "§c우리는 이름 \"을 제안 할 수있다IrisWorld\" 대신?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c그 폴더는 이미 존재합니다!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§c그 폴더는 이미 존재합니다!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e중 하나를 시도: overworld, 바닐라, 플랫, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e중 하나를 시도: overworld, 바닐라, 플랫, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§c창조 중 발생한 예외. 자세한 내용은 콘솔을 참조하십시오.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§a세상을 성공적으로 만들었습니다!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§a세상을 성공적으로 만들었습니다!",
|
||||||
"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.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.preparing_world_files_bukkit_yml_next_startup": "§e세계 파일 및 준비 bukkit.yml 다음 시작...",
|
||||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§c차원을 위한 단계 세계 파일에 실패 \"{value}\".",
|
"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.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.failed_update_bukkit_yml": "§c업데이트 실패 bukkit.yml: {value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c지정된 플레이어는 존재하지 않습니다.",
|
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c지정된 플레이어는 존재하지 않습니다.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§a아이리스 v{value} Volmit 소프트웨어",
|
"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.name_world_create": "만들 월드 이름",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "월드 생성에 사용할 차원 또는 팩",
|
"iris.director.commandiris.param.dimension_pack_create_world_with": "월드 생성에 사용할 차원 또는 팩",
|
||||||
"iris.director.commandiris.param.seed_generate_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.param.replace_exact_existing_world_slot_next_restart": "다음 재시작 시 기존의 정확한 월드 슬롯 교체",
|
||||||
"iris.director.commandiris.director.teleport_another_world": "다른 월드로 순간이동합니다",
|
"iris.director.commandiris.director.teleport_another_world": "다른 월드로 순간이동합니다",
|
||||||
"iris.director.commandiris.param.world_teleport": "순간이동할 월드",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cAr galime pasiūlyti pavadinimą \"IrisWorld\"vietoj to?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cTas aplankas jau egzistuoja!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cTas aplankas jau egzistuoja!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePabandykite vieną iš: per pasaulį, vanilla, butas, pabaiga",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§ePabandykite vieną iš: per pasaulį, vanilla, butas, pabaiga",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cSukūrimo metu iškelta išimtis. Daugiau informacijos rasite konsolėje.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aSėkmingai sukūrė savo pasaulį!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aSėkmingai sukūrė savo pasaulį!",
|
||||||
"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.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.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.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.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.failed_update_bukkit_yml": "§cNepavyko atnaujinti bukkit.yml: {value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cNurodytasis žaidėjas neegzistuoja.",
|
"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",
|
"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.name_world_create": "Kuriamo pasaulio pavadinimas",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "dimensija / paketas sukurti pasaulį su",
|
"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.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.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.director.teleport_another_world": "Teleportas į kitą pasaulį",
|
||||||
"iris.director.commandiris.param.world_teleport": "Pasaulis teleportui į",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cMogen we de naam voorstellen \"IrisWorldIn plaats daarvan?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cDie map bestaat al!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cDie map bestaat al!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProbeer een van: overwereld, vanilla, plat, einde",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eProbeer een van: overwereld, vanilla, plat, einde",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cUitzondering bij de schepping. Zie de console voor meer details.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aSuccesvol jullie wereld gecreëerd!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aSuccesvol jullie wereld gecreëerd!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cDe opgegeven speler bestaat niet.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris /{value} door Volmit Software",
|
"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.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.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.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.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.director.teleport_another_world": "Teleporteren naar een andere wereld",
|
||||||
"iris.director.commandiris.param.world_teleport": "Wereld te teleporteren naar",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cCzy możemy zasugerować nazwę \"IrisWorld\"zamiast?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cTen folder już istnieje!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cTen folder już istnieje!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eSpróbuj jeden z: zaświaty, vanilla, płaski, koniec",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eSpróbuj jeden z: zaświaty, vanilla, płaski, koniec",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cWyjątek podniesiony podczas tworzenia. Więcej szczegółów znajdziesz w konsoli.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aZ powodzeniem stworzyłeś swój świat!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aZ powodzeniem stworzyłeś swój świat!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cOkreślony gracz nie istnieje.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value} przez Volmit Software",
|
"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.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.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.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.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.director.teleport_another_world": "Teleport do innego świata",
|
||||||
"iris.director.commandiris.param.world_teleport": "Świat teleportować do",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cPodemos sugerir o nome \"IrisWorld\" em vez disso?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cEssa pasta já existe!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cEssa pasta já existe!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eExperimente um de: overworld, vanilla, flat, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eExperimente um de: overworld, vanilla, flat, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cExceção levantada durante a criação. Veja o console para mais detalhes.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aCriou com sucesso o seu mundo!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aCriou com sucesso o seu mundo!",
|
||||||
"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.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.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.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.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.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.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",
|
"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.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.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.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.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.director.teleport_another_world": "Teletransporte para outro mundo",
|
||||||
"iris.director.commandiris.param.world_teleport": "Mundo para teletransportar",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cМожно предложить название \"IrisWorldВместо этого?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cЭта папка уже существует!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cЭта папка уже существует!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eПопробуйте один из вариантов: overworld, vanilla, flat, theend",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eПопробуйте один из вариантов: overworld, vanilla, flat, theend",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cИсключение, возникшее при сотворении. Смотрите консоль для более подробной информации.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aУспешно создал свой мир!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aУспешно создал свой мир!",
|
||||||
"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.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.preparing_world_files_bukkit_yml_next_startup": "§eПодготовка мировых файлов и bukkit.yml Для следующего стартапа...",
|
||||||
"iris.bukkit.commandiris.failed_stage_world_files_dimension": "§cНе удалось создать мировые файлы для измерения\".{value}\".",
|
"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.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.failed_update_bukkit_yml": "§cНе удалось обновить bukkit.yml: {value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cУказанного игрока не существует.",
|
"iris.bukkit.commandiris.specified_player_does_not_exist": "§cУказанного игрока не существует.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aИрис v{value} Разработчик Volmit Software",
|
"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.name_world_create": "Название мира для создания",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "Размер/пакет для создания мира",
|
"iris.director.commandiris.param.dimension_pack_create_world_with": "Размер/пакет для создания мира",
|
||||||
"iris.director.commandiris.param.seed_generate_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.param.replace_exact_existing_world_slot_next_restart": "Заменить точный существующий слот мира при следующем перезапуске",
|
||||||
"iris.director.commandiris.director.teleport_another_world": "Телепорт в другой мир",
|
"iris.director.commandiris.director.teleport_another_world": "Телепорт в другой мир",
|
||||||
"iris.director.commandiris.param.world_teleport": "Телепортироваться в мир",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cAdımı önerebiliriz \"IrisWorldBunun yerine?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cBu klasör zaten var!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cBu klasör zaten var!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eBirini deneyin: Overworld, vanilla, düz, son",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eBirini deneyin: Overworld, vanilla, düz, son",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cYaratılış sırasında ortaya çıktı. Konsolu daha fazla ayrıntı için görün.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aBaşarılı bir şekilde dünyanızı yarattı!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aBaşarılı bir şekilde dünyanızı yarattı!",
|
||||||
"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.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.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.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.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.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.specified_player_does_not_exist": "§cBelirtilen oyuncu mevcut değildir.",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris{value} Volmit Software",
|
"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.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.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.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.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.director.teleport_another_world": "Teleport başka bir dünyaya",
|
||||||
"iris.director.commandiris.param.world_teleport": "Dünya teleport'a",
|
"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.may_we_suggest_name_irisworld_instead_2": "§cChúng ta có thể đề nghị cái tên \"IrisWorldThay vào đó?",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§cThư mục đó đã có!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§cThư mục đó đã có!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eHãy thử một trong những: ngoài thế giới, vanilla, bằng phẳng, kết thúc",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§eHãy thử một trong những: ngoài thế giới, vanilla, bằng phẳng, kết thúc",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§cNgoại lệ được nuôi lớn trong suốt quá trình sáng tạo. Xem bảng điều khiển để biết thêm chi tiết.",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§aThành công trong việc tạo ra thế giới của anh!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§aThành công trong việc tạo ra thế giới của anh!",
|
||||||
"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.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.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.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.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.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.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",
|
"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.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.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.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.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.director.teleport_another_world": "Name",
|
||||||
"iris.director.commandiris.param.world_teleport": "Thế giới có thể dịch chuyển",
|
"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.may_we_suggest_name_irisworld_instead_2": "§c建议改用名称 \"IrisWorld\"。",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c该文件夹已存在!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§c该文件夹已存在!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可尝试:overworld、vanilla、flat 或 the_end",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可尝试:overworld、vanilla、flat 或 the_end",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§c创建期间发生异常。详情请查看控制台。",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§a世界创建成功!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§a世界创建成功!",
|
||||||
"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.runtime_world_creation_is_disabled_on_folia": "§eFolia 上已禁用运行时世界创建。",
|
||||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e正在为下次启动准备世界文件和 bukkit.yml...",
|
"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.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.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.failed_update_bukkit_yml": "§c无法更新 bukkit.yml:{value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定的玩家不存在。",
|
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定的玩家不存在。",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value},由 Volmit Software 开发",
|
"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.name_world_create": "要创建的世界名称",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "用于创建世界的维度包",
|
"iris.director.commandiris.param.dimension_pack_create_world_with": "用于创建世界的维度包",
|
||||||
"iris.director.commandiris.param.seed_generate_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.param.replace_exact_existing_world_slot_next_restart": "在下次重启时替换指定的现有世界槽位",
|
||||||
"iris.director.commandiris.director.teleport_another_world": "传送到另一个世界",
|
"iris.director.commandiris.director.teleport_another_world": "传送到另一个世界",
|
||||||
"iris.director.commandiris.param.world_teleport": "要传送到的世界",
|
"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.may_we_suggest_name_irisworld_instead_2": "§c建議改用名稱 \"IrisWorld\"。",
|
||||||
"iris.bukkit.commandiris.that_folder_already_exists": "§c該資料夾已存在!",
|
"iris.bukkit.commandiris.that_folder_already_exists": "§c該資料夾已存在!",
|
||||||
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可嘗試:overworld、vanilla、flat 或 the_end",
|
"iris.bukkit.commandiris.try_one_overworld_vanilla_flat_theend": "§e可嘗試:overworld、vanilla、flat 或 the_end",
|
||||||
"iris.bukkit.commandiris.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.exception_raised_during_creation_see_console_more_details": "§c建立期間發生例外。詳細資訊請查看主控台。",
|
||||||
"iris.bukkit.commandiris.successfully_created_your_world": "§a世界建立成功!",
|
"iris.bukkit.commandiris.successfully_created_your_world": "§a世界建立成功!",
|
||||||
"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.runtime_world_creation_is_disabled_on_folia": "§eFolia 上已停用執行階段世界建立。",
|
||||||
"iris.bukkit.commandiris.preparing_world_files_bukkit_yml_next_startup": "§e正在為下次啟動準備世界檔案和 bukkit.yml...",
|
"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.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.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.failed_update_bukkit_yml": "§c無法更新 bukkit.yml:{value}",
|
||||||
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定的玩家不存在。",
|
"iris.bukkit.commandiris.specified_player_does_not_exist": "§c指定的玩家不存在。",
|
||||||
"iris.bukkit.commandiris.iris_v_by_volmit_software": "§aIris v.{value},由 Volmit Software 開發",
|
"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.name_world_create": "要建立的世界名稱",
|
||||||
"iris.director.commandiris.param.dimension_pack_create_world_with": "用於建立世界的維度包",
|
"iris.director.commandiris.param.dimension_pack_create_world_with": "用於建立世界的維度包",
|
||||||
"iris.director.commandiris.param.seed_generate_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.param.replace_exact_existing_world_slot_next_restart": "在下次重新啟動時取代指定的現有世界槽位",
|
||||||
"iris.director.commandiris.director.teleport_another_world": "傳送到另一個世界",
|
"iris.director.commandiris.director.teleport_another_world": "傳送到另一個世界",
|
||||||
"iris.director.commandiris.param.world_teleport": "要傳送到的世界",
|
"iris.director.commandiris.param.world_teleport": "要傳送到的世界",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public class ExactWorldSlotPathPolicyTest {
|
public class ExactWorldSlotPathPolicyTest {
|
||||||
@@ -53,6 +54,7 @@ public class ExactWorldSlotPathPolicyTest {
|
|||||||
assertEquals(canonicalRoot, target.levelRoot());
|
assertEquals(canonicalRoot, target.levelRoot());
|
||||||
assertEquals(canonicalRoot.resolve(expectation.relativePath()), target.worldDirectory());
|
assertEquals(canonicalRoot.resolve(expectation.relativePath()), target.worldDirectory());
|
||||||
}
|
}
|
||||||
|
assertFalse(Files.exists(canonicalRoot.resolve("dimensions")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.core;
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -79,6 +80,23 @@ public class IrisDatapackCompilerInputFingerprintTest {
|
|||||||
assertNotEquals(initial, fingerprint(pack, false, "compiler-b"));
|
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
|
@Test
|
||||||
public void compilerInputsRejectNestedSymbolicLinks() throws Exception {
|
public void compilerInputsRejectNestedSymbolicLinks() throws Exception {
|
||||||
Path pack = activePack("unsafe-input");
|
Path pack = activePack("unsafe-input");
|
||||||
@@ -146,6 +164,7 @@ public class IrisDatapackCompilerInputFingerprintTest {
|
|||||||
assertEquals(compilerRoots, compilationRoots);
|
assertEquals(compilerRoots, compilationRoots);
|
||||||
assertTrue(!IrisDatapackCompiler.computeInputFingerprint(
|
assertTrue(!IrisDatapackCompiler.computeInputFingerprint(
|
||||||
compilerRoots,
|
compilerRoots,
|
||||||
|
List.of(),
|
||||||
false,
|
false,
|
||||||
"compiler-a").isBlank());
|
"compiler-a").isBlank());
|
||||||
}
|
}
|
||||||
@@ -163,9 +182,27 @@ public class IrisDatapackCompilerInputFingerprintTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String fingerprint(Path pack, boolean adjustVanillaHeight, String compilerIdentity) throws IOException {
|
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(
|
return IrisDatapackCompiler.computeInputFingerprint(
|
||||||
List.of(pack.toFile()),
|
List.of(pack.toFile()),
|
||||||
|
bindings,
|
||||||
adjustVanillaHeight,
|
adjustVanillaHeight,
|
||||||
compilerIdentity);
|
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;
|
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.DataVersion;
|
||||||
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
|
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class IrisDatapackCompilerTest {
|
public class IrisDatapackCompilerTest {
|
||||||
@@ -42,6 +44,7 @@ public class IrisDatapackCompilerTest {
|
|||||||
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
||||||
packRoots,
|
packRoots,
|
||||||
new KList<File>().qadd(datapackRoot.toFile()),
|
new KList<File>().qadd(datapackRoot.toFile()),
|
||||||
|
List.of(),
|
||||||
new DataFixerV1217(),
|
new DataFixerV1217(),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -72,6 +75,7 @@ public class IrisDatapackCompilerTest {
|
|||||||
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
||||||
List.of(packRoot.toFile()),
|
List.of(packRoot.toFile()),
|
||||||
new KList<File>().qadd(datapackRoot.toFile()),
|
new KList<File>().qadd(datapackRoot.toFile()),
|
||||||
|
List.of(),
|
||||||
new DataFixerV1217(),
|
new DataFixerV1217(),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -106,6 +110,7 @@ public class IrisDatapackCompilerTest {
|
|||||||
IrisDatapackCompiler.compile(
|
IrisDatapackCompiler.compile(
|
||||||
List.of(),
|
List.of(),
|
||||||
new KList<File>().qadd(datapackRoot.toFile()),
|
new KList<File>().qadd(datapackRoot.toFile()),
|
||||||
|
List.of(),
|
||||||
new DataFixerV1217(),
|
new DataFixerV1217(),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -136,6 +141,7 @@ public class IrisDatapackCompilerTest {
|
|||||||
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
IrisDatapackCompiler.CompilationResult result = IrisDatapackCompiler.compile(
|
||||||
List.of(),
|
List.of(),
|
||||||
new KList<File>().qadd(datapackRoot.toFile()),
|
new KList<File>().qadd(datapackRoot.toFile()),
|
||||||
|
List.of(),
|
||||||
new DataFixerV1217(),
|
new DataFixerV1217(),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -147,7 +153,112 @@ public class IrisDatapackCompilerTest {
|
|||||||
assertFalse(Files.exists(stale));
|
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 {
|
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("dimensions"));
|
||||||
Files.createDirectories(root.resolve("biomes"));
|
Files.createDirectories(root.resolve("biomes"));
|
||||||
Files.writeString(
|
Files.writeString(
|
||||||
@@ -155,14 +266,14 @@ public class IrisDatapackCompilerTest {
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"name": "Test Dimension",
|
"name": "Test Dimension",
|
||||||
"environment": "NORMAL",
|
"environment": "%s",
|
||||||
"logicalHeight": 256,
|
"logicalHeight": 256,
|
||||||
"dimensionHeight": {
|
"dimensionHeight": {
|
||||||
"min": -64,
|
"min": -64,
|
||||||
"max": 320
|
"max": 320
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""",
|
""".formatted(environment),
|
||||||
StandardCharsets.UTF_8
|
StandardCharsets.UTF_8
|
||||||
);
|
);
|
||||||
Files.writeString(
|
Files.writeString(
|
||||||
@@ -182,4 +293,12 @@ public class IrisDatapackCompilerTest {
|
|||||||
StandardCharsets.UTF_8
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
@@ -86,6 +87,23 @@ public class IrisStartupValidationTest {
|
|||||||
assertEquals("restart boundary", IrisStartupValidation.denialReason().orElseThrow());
|
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
|
@Test
|
||||||
public void packValidationInfrastructureFailureDeniesCreation() {
|
public void packValidationInfrastructureFailureDeniesCreation() {
|
||||||
IrisStartupValidation.begin();
|
IrisStartupValidation.begin();
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import java.nio.file.Path;
|
|||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class IrisWorldStorageTest {
|
public class IrisWorldStorageTest {
|
||||||
@Rule
|
@Rule
|
||||||
@@ -45,6 +47,113 @@ public class IrisWorldStorageTest {
|
|||||||
assertEquals(new NamespacedKey("iris", "iris_world"), IrisWorldStorage.keyFromName("Iris World", "world"));
|
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
|
@Test
|
||||||
public void managedKeyRejectsMainWorldsAndUnsafePaths() {
|
public void managedKeyRejectsMainWorldsAndUnsafePaths() {
|
||||||
assertEquals(new NamespacedKey("iris", "iris_world"),
|
assertEquals(new NamespacedKey("iris", "iris_world"),
|
||||||
@@ -124,9 +233,26 @@ public class IrisWorldStorageTest {
|
|||||||
Path dimensions = Files.createDirectories(levelRoot.toPath().resolve("dimensions"));
|
Path dimensions = Files.createDirectories(levelRoot.toPath().resolve("dimensions"));
|
||||||
Path outside = temporaryFolder.newFolder("outside").toPath();
|
Path outside = temporaryFolder.newFolder("outside").toPath();
|
||||||
Files.createSymbolicLink(dimensions.resolve("iris"), outside);
|
Files.createSymbolicLink(dimensions.resolve("iris"), outside);
|
||||||
|
NamespacedKey key = new NamespacedKey("iris", "probe");
|
||||||
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> IrisWorldStorage.requireSafeManagedDimensionRoot(
|
assertThrows(IllegalArgumentException.class, () -> IrisWorldStorage.requireSafeManagedDimensionRoot(
|
||||||
levelRoot,
|
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;
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.WorldSlotKey;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -13,6 +14,7 @@ import java.nio.channels.ServerSocketChannel;
|
|||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -26,6 +28,112 @@ public class BukkitWorldConfigurationTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
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
|
@Test
|
||||||
public void registersAndRemovesWorldAtomically() throws Exception {
|
public void registersAndRemovesWorldAtomically() throws Exception {
|
||||||
File configuration = temporaryFolder.newFile("bukkit.yml");
|
File configuration = temporaryFolder.newFile("bukkit.yml");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.core.lifecycle;
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
import art.arcane.iris.core.WorldRemovalPathPolicy;
|
import art.arcane.iris.core.WorldRemovalPathPolicy;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
@@ -112,6 +114,34 @@ public class IrisWorldRemovalServiceTest {
|
|||||||
assertTrue(result.registryChanged());
|
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
|
@Test
|
||||||
public void registryFailureStopsBeforeFilesystemDeletion() throws Exception {
|
public void registryFailureStopsBeforeFilesystemDeletion() throws Exception {
|
||||||
LifecycleOperationCoordinator coordinator = new LifecycleOperationCoordinator();
|
LifecycleOperationCoordinator coordinator = new LifecycleOperationCoordinator();
|
||||||
|
|||||||
+60
-18
@@ -238,6 +238,57 @@ public class WorldReplacementBootstrapTest {
|
|||||||
assertFalse(Files.exists(paths(first).backup()));
|
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
|
@Test
|
||||||
public void roundTripsBlankAndWhitespaceOriginalGenerators() throws Exception {
|
public void roundTripsBlankAndWhitespaceOriginalGenerators() throws Exception {
|
||||||
for (String generator : List.of("", " ")) {
|
for (String generator : List.of("", " ")) {
|
||||||
@@ -260,24 +311,15 @@ public class WorldReplacementBootstrapTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsIrisWorldKeysThatCollideWithConfiguredVanillaAliases() {
|
public void usesPaperStartupAliasesForIrisReplacementJournals() {
|
||||||
WorldGeneratorSnapshot original = new WorldGeneratorSnapshot(false, false, false, null, false, null);
|
assertEquals(
|
||||||
for (String alias : List.of("world", "world_nether", "world_the_end")) {
|
"world_iris_moon",
|
||||||
Transaction transaction = new Transaction(
|
WorldReplacementJournal.logicalWorldName(levelRoot, new WorldSlotKey("iris", "moon"))
|
||||||
UUID.randomUUID(),
|
);
|
||||||
new WorldSlotKey("iris", alias),
|
assertEquals(
|
||||||
alias,
|
"world_iris_world_nether",
|
||||||
levelRoot,
|
WorldReplacementJournal.logicalWorldName(levelRoot, new WorldSlotKey("iris", "world_nether"))
|
||||||
"underworld",
|
);
|
||||||
SEED,
|
|
||||||
"0".repeat(64),
|
|
||||||
original,
|
|
||||||
false,
|
|
||||||
Phase.ARMED
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThrows(IOException.class, () -> WorldReplacementJournal.resolveTarget(transaction, levelRoot));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+39
-3
@@ -17,6 +17,7 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
@@ -67,7 +68,10 @@ public class WorldReplacementFilesystemTest {
|
|||||||
() -> WorldReplacementFilesystem.requireExistingTarget(paths)
|
() -> 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.target()));
|
||||||
assertFalse(Files.exists(paths.stage()));
|
assertFalse(Files.exists(paths.stage()));
|
||||||
assertFalse(Files.exists(paths.backup()));
|
assertFalse(Files.exists(paths.backup()));
|
||||||
@@ -92,7 +96,7 @@ public class WorldReplacementFilesystemTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsUnmigratedRetainedWorldBeforePublication() throws Exception {
|
public void rejectsIncompleteCurrentPaperWorldBeforePublication() throws Exception {
|
||||||
WorldReplacementFilesystem.ReplacementPaths paths = paths("missing-paper-metadata", TRANSACTION_ID);
|
WorldReplacementFilesystem.ReplacementPaths paths = paths("missing-paper-metadata", TRANSACTION_ID);
|
||||||
Files.createDirectories(paths.target());
|
Files.createDirectories(paths.target());
|
||||||
Files.writeString(paths.target().resolve("original.txt"), "original");
|
Files.writeString(paths.target().resolve("original.txt"), "original");
|
||||||
@@ -109,7 +113,7 @@ public class WorldReplacementFilesystemTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsUnmigratedRetainedWorldAtAdmission() throws Exception {
|
public void rejectsIncompleteCurrentPaperWorldAtAdmission() throws Exception {
|
||||||
WorldReplacementFilesystem.ReplacementPaths paths = paths("admit-missing-paper-metadata", TRANSACTION_ID);
|
WorldReplacementFilesystem.ReplacementPaths paths = paths("admit-missing-paper-metadata", TRANSACTION_ID);
|
||||||
Files.createDirectories(paths.target());
|
Files.createDirectories(paths.target());
|
||||||
Files.writeString(paths.target().resolve("original.txt"), "original");
|
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
|
@Test
|
||||||
public void rejectsMalformedAndCrossTransactionPaths() throws Exception {
|
public void rejectsMalformedAndCrossTransactionPaths() throws Exception {
|
||||||
Path parent = temporaryFolder.newFolder("invalid-paths").toPath();
|
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 org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class AtomicDirectoryPublisherTest {
|
public class AtomicDirectoryPublisherTest {
|
||||||
@@ -52,4 +54,21 @@ public class AtomicDirectoryPublisherTest {
|
|||||||
assertTrue(Files.isDirectory(target));
|
assertTrue(Files.isDirectory(target));
|
||||||
assertEquals("old", Files.readString(target.resolve("value.txt")));
|
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;
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
@@ -49,6 +50,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
try {
|
try {
|
||||||
Path dataDirectory = root.resolve("plugins/Iris");
|
Path dataDirectory = root.resolve("plugins/Iris");
|
||||||
DefaultPackBootstrapProvisioner.ProvisionOptions options = new DefaultPackBootstrapProvisioner.ProvisionOptions(
|
DefaultPackBootstrapProvisioner.ProvisionOptions options = new DefaultPackBootstrapProvisioner.ProvisionOptions(
|
||||||
|
List.of(),
|
||||||
List.of(),
|
List.of(),
|
||||||
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
|
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
|
||||||
Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
|
Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
|
||||||
@@ -71,7 +73,12 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
assertTrue(result.packRoots().isEmpty());
|
assertTrue(result.packRoots().isEmpty());
|
||||||
assertEquals(0, requests.get());
|
assertEquals(0, requests.get());
|
||||||
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("pack.mcmeta")));
|
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 {
|
} finally {
|
||||||
server.stop(0);
|
server.stop(0);
|
||||||
delete(root);
|
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/overworld/worldgen/biome/bootstrap_biome.json")));
|
||||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/underworld/worldgen/biome/underworld_biome.json")));
|
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/underworld/worldgen/biome/underworld_biome.json")));
|
||||||
assertFalse(Files.exists(dataDirectory.resolve("bootstrap/datapack")));
|
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());
|
assertTrue(DefaultPackBootstrapProvisioner.wasProvisionedThisStartup());
|
||||||
Properties marker = loadProperties(dataDirectory.resolve("bootstrap/provisioned.properties"));
|
Properties marker = loadProperties(dataDirectory.resolve("bootstrap/provisioned.properties"));
|
||||||
assertEquals("true", marker.getProperty("pack.overworld.managed"));
|
assertEquals("true", marker.getProperty("pack.overworld.managed"));
|
||||||
assertEquals("true", marker.getProperty("pack.underworld.managed"));
|
assertEquals("true", marker.getProperty("pack.underworld.managed"));
|
||||||
assertEquals("underworld", marker.getProperty("pack.underworld.requiredDimension"));
|
assertEquals("underworld", marker.getProperty("pack.underworld.requiredDimension"));
|
||||||
delete(installed.packRoots().get("underworld"));
|
delete(installed.packRoots().get("underworld"));
|
||||||
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, options.packs()));
|
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(
|
||||||
|
dataDirectory,
|
||||||
|
root,
|
||||||
|
options.packs(),
|
||||||
|
options.bindings()
|
||||||
|
));
|
||||||
} finally {
|
} finally {
|
||||||
server.stop(0);
|
server.stop(0);
|
||||||
delete(root);
|
delete(root);
|
||||||
@@ -309,7 +326,12 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("data/overworld/worldgen/biome/local_biome.json")));
|
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);
|
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(
|
DefaultPackBootstrapProvisioner.ProvisionResult updated = DefaultPackBootstrapProvisioner.provision(
|
||||||
dataDirectory,
|
dataDirectory,
|
||||||
ignored -> {
|
ignored -> {
|
||||||
@@ -338,7 +360,12 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
writePack(dataDirectory.resolve("packs/second"), "second", "second_biome");
|
writePack(dataDirectory.resolve("packs/second"), "second", "second_biome");
|
||||||
writePack(root.resolve("dimensions/example/world/iris/pack"), "world_local", "world_local_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(
|
DefaultPackBootstrapProvisioner.ProvisionResult updated = DefaultPackBootstrapProvisioner.provision(
|
||||||
dataDirectory,
|
dataDirectory,
|
||||||
ignored -> {
|
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(
|
private static DefaultPackBootstrapProvisioner.ProvisionOptions options(
|
||||||
HttpServer server,
|
HttpServer server,
|
||||||
Path serverRoot,
|
Path serverRoot,
|
||||||
@@ -610,6 +687,7 @@ public class DefaultPackBootstrapProvisionerTest {
|
|||||||
"underworld"
|
"underworld"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
List.of(),
|
||||||
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
|
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
|
||||||
Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
|
Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
|||||||
Reference in New Issue
Block a user