Merged replace target refusals

- Modded /iris replace refuses dimensions no level stem defines
- Replacements count as applied only when the live level generates from Iris
- Unknown replace types list installed packs
This commit is contained in:
Brian Neumann-Fopiano
2026-10-15 19:01:06 -04:00
27 changed files with 542 additions and 55 deletions
@@ -18,6 +18,10 @@
package art.arcane.iris.modded;
import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs;
import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogStore;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.json.JSONObject;
@@ -27,15 +31,20 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
public final class ModdedDimensionReplacements {
private static final String GENERATOR_TYPE = "irisworldgen:iris";
public static final String IRIS_GENERATOR = "irisworldgen:iris";
private static final Set<String> VANILLA_LEVELS = Set.of("minecraft:overworld", "minecraft:the_nether",
"minecraft:the_end");
private static volatile RecordedCatalog recordedCatalog;
private static final String PLACEHOLDER_BIOME_SOURCE = "minecraft:fixed";
private static final String PLACEHOLDER_BIOME = "minecraft:plains";
private static final String DIMENSIONS = "dimensions";
@@ -49,7 +58,7 @@ public final class ModdedDimensionReplacements {
biomeSource.put("type", PLACEHOLDER_BIOME_SOURCE);
biomeSource.put("biome", PLACEHOLDER_BIOME);
JSONObject generator = new JSONObject();
generator.put("type", GENERATOR_TYPE);
generator.put("type", IRIS_GENERATOR);
generator.put("biome_source", biomeSource);
generator.put("dimension", replacement.packRef());
JSONObject stem = new JSONObject();
@@ -72,8 +81,13 @@ public final class ModdedDimensionReplacements {
}
public static void writeLevelStems(Path datapackRoot, Map<String, ModdedDimensionReplacement> configured,
StagedTypes staged, Path worldRoot) throws IOException {
StagedTypes staged, Path worldRoot, ReplaceableLevels levels) throws IOException {
for (ModdedDimensionReplacement replacement : new TreeMap<>(configured).values()) {
if (!levels.replaceable(replacement.levelKey())) {
ModdedIrisLog.error(unreplaceableReason(replacement.levelKey(), levels)
+ " Iris writes no dimension override for it.");
continue;
}
String typeRef = staged.typeRef(replacement, worldRoot);
if (typeRef == null) {
skipUnstaged(replacement, staged, worldRoot);
@@ -89,6 +103,68 @@ public final class ModdedDimensionReplacements {
return canonical(ModdedModConfig.get().dimensionReplacements(), IrisPlatforms.get().packsFolderNoCreate().toPath());
}
public static Map<String, ModdedDimensionReplacement> stageable(Map<String, ModdedDimensionReplacement> configured,
ReplaceableLevels levels) {
Map<String, ModdedDimensionReplacement> stageable = new TreeMap<>();
for (ModdedDimensionReplacement replacement : configured.values()) {
if (levels.replaceable(replacement.levelKey())) {
stageable.put(replacement.levelKey(), replacement);
}
}
return Map.copyOf(stageable);
}
public static ReplaceableLevels replaceableLevels(Path worldRoot, Map<String, ModdedDimensionReplacement> configured)
throws IOException {
if (configured.isEmpty()) {
return ReplaceableLevels.unknown();
}
return replaceableLevels(worldRoot);
}
public static ReplaceableLevels replaceableLevels(Path worldRoot) throws IOException {
Set<String> applied = worldRoot == null
? Set.of()
: ModdedReplacementReconciler.readState(worldRoot).applied().keySet();
Optional<WorldgenCatalog> catalog = ModdedWorldgenCatalogs.latest();
if (catalog.isEmpty()) {
catalog = recordedCatalog();
}
return catalog.isEmpty()
? ReplaceableLevels.unknown()
: ReplaceableLevels.of(catalog.get().levelStems(), applied);
}
public static String unreplaceableReason(String levelKey, ReplaceableLevels levels) {
if (!levels.known()) {
return "Iris refuses to replace " + levelKey + ": it has no record of which dimensions this server loads"
+ " from level stems yet. Start the server once so Iris can read the dimension registry.";
}
return "Iris refuses to replace " + levelKey + ": it is not a dimension the server loads from a level stem;"
+ " a mod creates it while the server runs, so its data stays untouched.";
}
private static Optional<WorldgenCatalog> recordedCatalog() {
Path file = ModdedWorldgenCatalogs.catalogFile();
if (!Files.isRegularFile(file)) {
return Optional.empty();
}
try {
long modified = Files.getLastModifiedTime(file).toMillis();
RecordedCatalog cached = recordedCatalog;
if (cached != null && cached.file().equals(file) && cached.modified() == modified) {
return Optional.of(cached.catalog());
}
WorldgenCatalog catalog = WorldgenCatalogStore.read(file);
recordedCatalog = new RecordedCatalog(file, modified, catalog);
return Optional.of(catalog);
} catch (IOException | RuntimeException unreadable) {
ModdedIrisLog.error("Iris could not read the recorded worldgen catalog at {}; only the vanilla dimensions are replaceable until the next start records it",
file, unreadable);
return Optional.empty();
}
}
public static OptionalLong loadedSeed(String levelKey) {
ModdedModConfig config = ModdedModConfig.loaded();
if (config == null) {
@@ -188,6 +264,39 @@ public final class ModdedDimensionReplacements {
return canonical.toString();
}
private record RecordedCatalog(Path file, long modified, WorldgenCatalog catalog) {
}
public record ReplaceableLevels(boolean known, Map<String, String> stemGenerators, Set<String> applied) {
public ReplaceableLevels {
stemGenerators = Map.copyOf(stemGenerators);
applied = Set.copyOf(applied);
}
public static ReplaceableLevels unknown() {
return new ReplaceableLevels(false, Map.of(), Set.of());
}
public static ReplaceableLevels of(List<CatalogLevelStem> stems, Set<String> applied) {
Map<String, String> generators = new TreeMap<>();
for (CatalogLevelStem stem : stems) {
generators.put(stem.key(), stem.generatorType());
}
return new ReplaceableLevels(true, generators, applied);
}
public boolean replaceable(String levelKey) {
if (VANILLA_LEVELS.contains(levelKey)) {
return true;
}
String generator = stemGenerators.get(levelKey);
if (generator == null) {
return false;
}
return !IRIS_GENERATOR.equals(generator) || applied.contains(levelKey);
}
}
public record StagedTypes(Map<String, String> installed, Map<Path, String> retained) {
public static StagedTypes create() {
return new StagedTypes(new LinkedHashMap<>(), new LinkedHashMap<>());
@@ -178,6 +178,7 @@ public final class ModdedEngineBootstrap {
reconcileSpawn(server, replacedOverworld);
if (!loader().clientEnvironment()) {
ModdedReplacementReconciler.recordApplied(server);
ModdedReplacementReconciler.stopUnpublishedEngines(server);
ModdedReplacementEntry.retireIfComplete();
}
ModdedWorldCheck.serverStarted(server);
@@ -324,7 +324,9 @@ public final class ModdedForcedDatapack {
false
);
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
digest.update(ModdedDimensionReplacements.fingerprint(ModdedDimensionReplacements.configured())
Map<String, ModdedDimensionReplacement> replacements = ModdedDimensionReplacements.configured();
digest.update(ModdedDimensionReplacements.fingerprint(ModdedDimensionReplacements.stageable(replacements,
ModdedDimensionReplacements.replaceableLevels(worldRootOrNull(), replacements)))
.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest.digest());
}
@@ -346,8 +348,9 @@ public final class ModdedForcedDatapack {
}
}
if (!ModdedEngineBootstrap.loader().clientEnvironment()) {
ModdedDimensionReplacements.writeLevelStems(stagingDirectory,
ModdedDimensionReplacements.configured(), stagedDimensionTypes, worldRoot);
Map<String, ModdedDimensionReplacement> replacements = ModdedDimensionReplacements.configured();
ModdedDimensionReplacements.writeLevelStems(stagingDirectory, replacements, stagedDimensionTypes, worldRoot,
ModdedDimensionReplacements.replaceableLevels(worldRoot, replacements));
}
IrisDatapackCompiler.installRetainedRegistrySources(packRoots, List.of(stagingDirectory.toFile()), fixer);
@@ -38,6 +38,7 @@ import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
@@ -72,9 +73,9 @@ public final class ModdedReplacementReconciler {
Path packsRoot = IrisPlatforms.get().packsFolderNoCreate().toPath();
Result result = reconcile(new Request(worldRoot, configured, config.replacementProblems(),
(ModdedDimensionReplacement replacement) -> ModdedDimensionReplacements.installed(replacement, packsRoot),
recoveryParent));
recoveryParent, ModdedDimensionReplacements.replaceableLevels(worldRoot, configured)));
if (!result.adoptedLevels().isEmpty()) {
ModdedIrisLog.info("Iris dimension replacement recorded for {}: their existing Iris generation history already uses the configured pack and seed",
ModdedIrisLog.info("Iris kept the existing terrain of {}: its Iris generation history already uses the configured pack and seed; the replacement is recorded once the level generates from it",
result.adoptedLevels());
}
if (!result.replacedWorlds().isEmpty()) {
@@ -101,7 +102,7 @@ public final class ModdedReplacementReconciler {
}
try {
recordApplied(worldRoot.get(), ModdedDimensionReplacements.configured(),
(String levelKey, String packRef) -> generatedSeed(server, levelKey, packRef));
(String levelKey) -> liveLevel(server, levelKey));
} catch (IOException | RuntimeException failure) {
ModdedIrisLog.error("Iris is stopping the server because it could not record the applied dimension replacements under {}; an unrecorded replacement would be moved aside again on the next start",
worldRoot.get(), failure);
@@ -109,15 +110,29 @@ public final class ModdedReplacementReconciler {
}
}
static Map<String, ModdedDimensionReplacement> recordApplied(Path worldRoot,
public static void stopUnpublishedEngines(NativeModdedServer server) {
for (String levelKey : ModdedWorldEngines.evictUnpublished(server,
ModdedModConfig.get().dimensionReplacements().keySet())) {
ModdedIrisLog.error("Iris stopped the engine it started for {}: a mod replaced that level with its own chunk generator while the server started",
levelKey);
}
}
static synchronized Map<String, ModdedDimensionReplacement> recordApplied(Path worldRoot,
Map<String, ModdedDimensionReplacement> configured,
GeneratedSeed generating) throws IOException {
LiveLevels levels) throws IOException {
State state = readState(worldRoot);
Map<String, ModdedDimensionReplacement> recorded = new TreeMap<>(state.applied());
for (ModdedDimensionReplacement replacement : new TreeMap<>(configured).values()) {
OptionalLong seed = generating.seed(replacement.levelKey(), replacement.packRef());
if (seed.isPresent() && (replacement.seed().isEmpty() || replacement.seed().equals(seed))) {
recorded.put(replacement.levelKey(), replacement.withSeed(seed.getAsLong()));
Optional<LiveLevel> live = levels.level(replacement.levelKey());
if (live.isPresent() && live.get().generates(replacement)) {
recorded.put(replacement.levelKey(), replacement.withSeed(live.get().seed()));
continue;
}
if (live.isPresent() && !live.get().iris()) {
recorded.remove(replacement.levelKey());
ModdedIrisLog.error("Iris replacement of {} is configured but the loaded level uses another chunk generator; a mod creates that level while the server runs, so Iris cannot replace it. Remove its dimensionReplacements entry from modded.json",
replacement.levelKey());
continue;
}
ModdedIrisLog.warn("Iris replacement of {} is configured but the level does not generate from {}{}; it stays unapplied",
@@ -130,7 +145,7 @@ public final class ModdedReplacementReconciler {
return Map.copyOf(recorded);
}
static Result reconcile(Request request) throws IOException {
static synchronized Result reconcile(Request request) throws IOException {
Path worldRoot = request.worldRoot();
requireReadableConfig(worldRoot, request.configProblems());
State state = readState(worldRoot);
@@ -149,6 +164,7 @@ public final class ModdedReplacementReconciler {
Map<String, ModdedDimensionReplacement> recorded = new TreeMap<>(applied);
Set<String> quarantined = new TreeSet<>();
Set<String> adopted = new TreeSet<>();
Set<String> unreplaceable = new TreeSet<>();
for (String level : changed) {
ModdedDimensionReplacement replacement = request.configured().get(level);
if (replacement == null) {
@@ -156,12 +172,15 @@ public final class ModdedReplacementReconciler {
quarantined.add(level);
continue;
}
if (!request.levels().replaceable(level)) {
unreplaceable.add(level);
ModdedIrisLog.error(ModdedDimensionReplacements.unreplaceableReason(level, request.levels())
+ " Remove its dimensionReplacements entry from modded.json.");
continue;
}
Path storage = replacement.storageFolder(worldRoot);
OptionalLong historySeed = refreshed.contains(level)
? OptionalLong.empty()
: matchingHistorySeed(storage, replacement);
if (historySeed.isPresent()) {
recorded.put(level, replacement.withSeed(historySeed.getAsLong()));
boolean keepHistory = !refreshed.contains(level) && matchingHistorySeed(storage, replacement).isPresent();
if (keepHistory) {
adopted.add(level);
} else if (request.installed().test(replacement)) {
quarantined.add(level);
@@ -205,7 +224,7 @@ public final class ModdedReplacementReconciler {
writeState(worldRoot, reconciled);
}
return new Result(Set.copyOf(quarantined), Set.copyOf(adopted), Set.copyOf(replacedWorlds.keySet()),
moved.isEmpty() ? null : recovery);
Set.copyOf(unreplaceable), moved.isEmpty() ? null : recovery);
}
public static boolean pending(Path worldRoot, String levelKey, Map<String, ModdedDimensionReplacement> configured)
@@ -320,16 +339,16 @@ public final class ModdedReplacementReconciler {
return packs.isEmpty() || packs.contains(replacement.pack());
}
private static OptionalLong generatedSeed(NativeModdedServer server, String levelKey, String packRef) {
private static Optional<LiveLevel> liveLevel(NativeModdedServer server, String levelKey) {
NativeWorld world = server.world(levelKey);
if (world == null) {
return OptionalLong.empty();
return Optional.empty();
}
IrisModdedChunkGenerator generator = NativeWorldGenerators.find(world, IrisModdedChunkGenerator.class);
if (generator == null || !packRef.equals(generator.dimensionKey())) {
return OptionalLong.empty();
if (generator == null) {
return Optional.of(new LiveLevel(false, "", 0L));
}
return OptionalLong.of(generator.visibleBiomeSeed());
return Optional.of(new LiveLevel(true, generator.dimensionKey(), generator.visibleBiomeSeed()));
}
private static void requireReadableConfig(Path worldRoot, List<String> configProblems) throws IOException {
@@ -395,8 +414,19 @@ public final class ModdedReplacementReconciler {
return section;
}
interface GeneratedSeed {
OptionalLong seed(String levelKey, String packRef);
interface LiveLevels {
Optional<LiveLevel> level(String levelKey);
}
record LiveLevel(boolean iris, String packRef, long seed) {
LiveLevel {
Objects.requireNonNull(packRef, "packRef");
}
boolean generates(ModdedDimensionReplacement replacement) {
return iris && packRef.equals(replacement.packRef())
&& (replacement.seed().isEmpty() || replacement.seed().getAsLong() == seed);
}
}
record State(Map<String, ModdedDimensionReplacement> applied, Map<String, ModdedDimensionReplacement> pending) {
@@ -409,9 +439,11 @@ public final class ModdedReplacementReconciler {
}
record Request(Path worldRoot, Map<String, ModdedDimensionReplacement> configured, List<String> configProblems,
Predicate<ModdedDimensionReplacement> installed, Path recoveryParent) {
Predicate<ModdedDimensionReplacement> installed, Path recoveryParent,
ModdedDimensionReplacements.ReplaceableLevels levels) {
}
record Result(Set<String> quarantinedLevels, Set<String> adoptedLevels, Set<String> replacedWorlds, Path recovery) {
record Result(Set<String> quarantinedLevels, Set<String> adoptedLevels, Set<String> replacedWorlds,
Set<String> unreplaceableLevels, Path recovery) {
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeDimensionRuntime;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
@@ -49,8 +50,10 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedWorldEngines {
@@ -92,6 +95,19 @@ public final class ModdedWorldEngines {
}
}
static List<String> evictUnpublished(NativeModdedServer server, Set<String> levelKeys) {
List<String> evicted = new ArrayList<>();
for (NativeWorld level : new ArrayList<>(ENGINES.keySet())) {
if (!levelKeys.contains(level.name())
|| NativeDimensionRuntime.sameWorld(server.world(level.name()), level)) {
continue;
}
evict(level);
evicted.add(level.name());
}
return evicted;
}
static void evictOrThrow(NativeWorld level) {
Engine[] removed = new Engine[1];
ENGINES.computeIfPresent(level, (NativeWorld ignored, Engine current) -> {
@@ -30,7 +30,11 @@ import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedModConfig;
import art.arcane.iris.modded.ModdedReplacementReconciler;
import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs;
import art.arcane.iris.modded.localization.ModdedCommandMessages;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.pack.PackDownloader;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
@@ -46,10 +50,15 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
@@ -135,6 +144,7 @@ final class ModdedReplaceCommands {
throw new IllegalArgumentException(MISSING_TARGET);
}
Path worldRoot = server.root();
requireReplaceableLevel(worldRoot, requested.levelKey());
requireNothingPending(worldRoot, requested.levelKey());
long seed = requested.seed().orElse(ModdedReplacementReconciler.applied(worldRoot, requested.levelKey())
.map((ModdedDimensionReplacement applied) -> applied.seed().getAsLong())
@@ -166,6 +176,26 @@ final class ModdedReplaceCommands {
return staged;
}
private static void requireReplaceableLevel(Path worldRoot, String levelKey) throws IOException {
Optional<WorldgenCatalog> catalog = ModdedWorldgenCatalogs.latest();
Set<String> applied = ModdedReplacementReconciler.applied(worldRoot, levelKey).isPresent()
? Set.of(levelKey)
: Set.of();
ModdedDimensionReplacements.ReplaceableLevels levels = catalog.isEmpty()
? ModdedDimensionReplacements.ReplaceableLevels.unknown()
: ModdedDimensionReplacements.ReplaceableLevels.of(catalog.get().levelStems(), applied);
if (levels.replaceable(levelKey)) {
return;
}
if (!levels.known()) {
throw new IllegalArgumentException(IrisLanguage.plain(
ModdedCommandMessages.MODDED_WORLD_COMMANDS_REPLACE_REGISTRY_NOT_READY));
}
throw new IllegalArgumentException(IrisLanguage.plain(
ModdedCommandMessages.MODDED_WORLD_COMMANDS_REPLACE_RUNTIME_LEVEL,
MessageArgument.untrusted("dimensionId", levelKey)));
}
private static void requireNothingPending(Path worldRoot, String levelKey) throws IOException {
Map<String, ModdedDimensionReplacement> configured = ModdedModConfig.get().dimensionReplacements();
if (ModdedReplacementReconciler.pending(worldRoot, levelKey, configured)) {
@@ -230,13 +260,52 @@ final class ModdedReplaceCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
MessageArgument.untrusted("dimension", resolvedType)));
IrisModdedCommands.fail(source, IrisLanguage.plain(
BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
List<String> installed = installedTypes(ModdedPackCommands.packsRoot());
if (!installed.isEmpty()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_WORLD_COMMANDS_REPLACE_INSTALLED_TYPES,
MessageArgument.untrusted("value", String.join(", ", installed))));
}
IrisModdedCommands.fail(source, IrisLanguage.plain(
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(packOf(resolvedType)))));
}
static List<String> installedTypes(File packsRoot) {
Set<String> types = new TreeSet<>();
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsRoot)) {
List<String> dimensionKeys = dimensionKeys(pack.toPath().resolve("dimensions"));
if (dimensionKeys.isEmpty()) {
continue;
}
types.add(pack.getName());
for (String dimensionKey : dimensionKeys) {
if (!dimensionKey.equals(pack.getName())) {
types.add(pack.getName() + ":" + dimensionKey);
}
}
}
return List.copyOf(types);
}
private static List<String> dimensionKeys(Path dimensions) {
if (!Files.isDirectory(dimensions)) {
return List.of();
}
List<String> keys = new ArrayList<>();
try (Stream<Path> files = Files.walk(dimensions)) {
for (Path file : files.filter(Files::isRegularFile).toList()) {
String relative = dimensions.relativize(file).toString().replace(File.separatorChar, '/');
if (relative.endsWith(".json")) {
keys.add(relative.substring(0, relative.length() - ".json".length()));
}
}
} catch (IOException | RuntimeException unreadable) {
ModdedIrisLog.warn("Iris could not list the dimensions of pack folder {}", dimensions.getParent(), unreadable);
}
return keys;
}
private static String packOf(String type) {
String value = type.trim();
int colon = value.indexOf(':');
@@ -1260,6 +1260,18 @@ public final class ModdedCommandMessages {
"iris.modded.moddedworldcommands.use_iris_world_create_name_pack_inject_one_without_restarting",
"Use /iris world create <name> <pack> to inject one without restarting."
);
public static final TextKey MODDED_WORLD_COMMANDS_REPLACE_RUNTIME_LEVEL = TextKey.of(
"iris.modded.moddedworldcommands.replace_runtime_level",
"{dimensionId}" + " is created by a mod while the server runs and is not defined as a dimension, so Iris cannot replace it."
);
public static final TextKey MODDED_WORLD_COMMANDS_REPLACE_REGISTRY_NOT_READY = TextKey.of(
"iris.modded.moddedworldcommands.replace_registry_not_ready",
"Iris is still reading this server's dimensions. Try again in a moment."
);
public static final TextKey MODDED_WORLD_COMMANDS_REPLACE_INSTALLED_TYPES = TextKey.of(
"iris.modded.moddedworldcommands.replace_installed_types",
"Installed packs: " + "{value}"
);
private static final List<MessageKey> KEYS = List.of(
IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS,
@@ -1574,7 +1586,10 @@ public final class ModdedCommandMessages {
MODDED_WORLD_COMMANDS_NO_IRIS_DIMENSIONS_ARE_CURRENTLY_LOADED_CREATE_ONE_WITH_IRIS,
MODDED_WORLD_COMMANDS_LOADED_IRIS_DIMENSIONS,
MODDED_WORLD_COMMANDS_MESSAGE_2,
MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING
MODDED_WORLD_COMMANDS_USE_IRIS_WORLD_CREATE_NAME_PACK_INJECT_ONE_WITHOUT_RESTARTING,
MODDED_WORLD_COMMANDS_REPLACE_RUNTIME_LEVEL,
MODDED_WORLD_COMMANDS_REPLACE_REGISTRY_NOT_READY,
MODDED_WORLD_COMMANDS_REPLACE_INSTALLED_TYPES
);
private ModdedCommandMessages() {
@@ -37,6 +37,58 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedDimensionReplacementsTest {
private static final ModdedDimensionReplacements.ReplaceableLevels STEMS = stems("aether:the_aether");
@Test
public void onlyLevelStemsTheServerCreatesItselfAreReplaceable() {
ModdedDimensionReplacements.ReplaceableLevels levels = new ModdedDimensionReplacements.ReplaceableLevels(true,
Map.of("minecraft:overworld", "minecraft:noise",
"examplemod:skylands", "examplemod:sky_generator",
"examplemod:applied", ModdedDimensionReplacements.IRIS_GENERATOR,
"ae2:spatial_storage", ModdedDimensionReplacements.IRIS_GENERATOR),
Set.of("examplemod:applied"));
assertTrue(levels.replaceable("minecraft:overworld"));
assertTrue(levels.replaceable("minecraft:the_end"));
assertTrue(levels.replaceable("examplemod:skylands"));
assertTrue(levels.replaceable("examplemod:applied"));
assertFalse(levels.replaceable("ae2:spatial_storage"));
assertFalse(levels.replaceable("ae2:runtime_only"));
}
@Test
public void withoutARecordedRegistryOnlyTheVanillaDimensionsAreReplaceable() {
ModdedDimensionReplacements.ReplaceableLevels unknown = ModdedDimensionReplacements.ReplaceableLevels.unknown();
assertFalse(unknown.known());
assertTrue(unknown.replaceable("minecraft:overworld"));
assertTrue(unknown.replaceable("minecraft:the_nether"));
assertFalse(unknown.replaceable("examplemod:skylands"));
}
@Test
public void unreplaceableLevelsGetNoLevelStemOverride() throws IOException {
Path temporary = Files.createTempDirectory("iris-replace-stage-runtime");
try {
Path datapack = temporary.resolve("datapack");
ModdedDimensionReplacements.StagedTypes staged = ModdedDimensionReplacements.StagedTypes.create();
staged.installed().put("overworld:overworld", "irisworldgen:packs/x/dimensions/y/dimension_type");
ModdedDimensionReplacements.writeLevelStems(datapack, Map.of(
"ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld"),
"minecraft:the_end", replacement("minecraft:the_end", "overworld")), staged,
temporary.resolve("world"), STEMS);
assertFalse(Files.exists(datapack.resolve("data/ae2/dimension/spatial_storage.json")));
assertTrue(Files.isRegularFile(datapack.resolve("data/minecraft/dimension/the_end.json")));
assertEquals(Set.of("minecraft:the_end"), ModdedDimensionReplacements.stageable(Map.of(
"ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld"),
"minecraft:the_end", replacement("minecraft:the_end", "overworld")), STEMS).keySet());
} finally {
ModdedReplacementReconcilerTest.deleteTree(temporary);
}
}
@Test
public void parsesPackAndPackDimensionReferences() {
ModdedDimensionReplacement samePack = replacement("iristest:skylands", "skypack");
@@ -170,7 +222,7 @@ public class ModdedDimensionReplacementsTest {
ModdedDimensionReplacements.writeLevelStems(datapack,
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")), staged,
temporary.resolve("world"));
temporary.resolve("world"), STEMS);
JSONObject stem = new JSONObject(Files.readString(datapack.resolve("data/aether/dimension/the_aether.json")));
assertEquals("irisworldgen:packs/x/dimensions/y/dimension_type", stem.getString("type"));
@@ -191,7 +243,7 @@ public class ModdedDimensionReplacementsTest {
"aether:the_aether");
ModdedDimensionReplacements.writeLevelStems(datapack,
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")), staged, worldRoot);
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")), staged, worldRoot, STEMS);
JSONObject stem = new JSONObject(Files.readString(datapack.resolve("data/aether/dimension/the_aether.json")));
assertEquals("aether:the_aether", stem.getString("type"));
@@ -211,9 +263,9 @@ public class ModdedDimensionReplacementsTest {
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky"));
ModdedDimensionReplacements.writeLevelStems(datapack, configured,
ModdedDimensionReplacements.StagedTypes.create(), worldRoot);
ModdedDimensionReplacements.StagedTypes.create(), worldRoot, STEMS);
ModdedDimensionReplacements.writeLevelStems(datapack, configured,
ModdedDimensionReplacements.StagedTypes.create(), null);
ModdedDimensionReplacements.StagedTypes.create(), null, STEMS);
assertFalse(Files.exists(datapack.resolve("data/aether/dimension/the_aether.json")));
} finally {
@@ -232,7 +284,7 @@ public class ModdedDimensionReplacementsTest {
IOException refused = assertThrows(IOException.class, () -> ModdedDimensionReplacements.writeLevelStems(
temporary.resolve("datapack"),
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")),
ModdedDimensionReplacements.StagedTypes.create(), worldRoot));
ModdedDimensionReplacements.StagedTypes.create(), worldRoot, STEMS));
assertTrue(refused.getMessage(), refused.getMessage().contains("aether:the_aether"));
assertTrue(refused.getMessage(), refused.getMessage().contains("skypack"));
@@ -252,7 +304,7 @@ public class ModdedDimensionReplacementsTest {
assertThrows(IOException.class, () -> ModdedDimensionReplacements.writeLevelStems(
temporary.resolve("datapack"),
Map.of("aether:the_aether", replacement("aether:the_aether", "skypack:sky")),
ModdedDimensionReplacements.StagedTypes.create(), worldRoot));
ModdedDimensionReplacements.StagedTypes.create(), worldRoot, STEMS));
} finally {
ModdedReplacementReconcilerTest.deleteTree(temporary);
}
@@ -280,6 +332,14 @@ public class ModdedDimensionReplacementsTest {
}
}
static ModdedDimensionReplacements.ReplaceableLevels stems(String... levelKeys) {
Map<String, String> generators = new LinkedHashMap<>();
for (String levelKey : levelKeys) {
generators.put(levelKey, "minecraft:noise");
}
return new ModdedDimensionReplacements.ReplaceableLevels(true, generators, Set.of());
}
static ModdedDimensionReplacement replacement(String levelKey, String type) {
return ModdedDimensionReplacement.parse(levelKey, type, OptionalLong.empty());
}
@@ -32,7 +32,6 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
@@ -46,6 +45,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedReplacementReconcilerTest {
private static final ModdedDimensionReplacements.ReplaceableLevels STEMS = ModdedDimensionReplacementsTest.stems(
"iristest:skylands", "iristest:unconfigured", "aether:the_aether");
@Test
public void stateRoundTripsAppliedAndPendingEntriesAndDefaultsToEmpty() throws IOException {
Path worldRoot = Files.createTempDirectory("iris-replace-state");
@@ -260,8 +262,7 @@ public class ModdedReplacementReconcilerTest {
assertEquals(Set.of("iristest:skylands"), result.adoptedLevels());
assertNull(result.recovery());
assertTrue(Files.isRegularFile(skylands.resolve("region/r.0.0.mca")));
assertEquals(Map.of("iristest:skylands", seeded("iristest:skylands", "skypack:sky", 5L)),
ModdedReplacementReconciler.readState(worldRoot).applied());
assertEquals(Map.of(), ModdedReplacementReconciler.readState(worldRoot).applied());
} finally {
deleteTree(temporary);
}
@@ -433,29 +434,38 @@ public class ModdedReplacementReconcilerTest {
}
@Test
public void recordAppliedStoresOnlyReplacementsThatGenerateFromTheirPackAndSeed() throws IOException {
public void recordAppliedStoresOnlyLevelsThatLiveGenerateFromTheirPackAndSeed() throws IOException {
Path temporary = Files.createTempDirectory("iris-replace-record");
Path worldRoot = temporary.resolve("world");
try {
writeApplied(worldRoot, seeded("iristest:kept", "overworld", 5L));
ModdedReplacementReconciler.writeState(worldRoot, new ModdedReplacementReconciler.State(Map.of(
"iristest:kept", seeded("iristest:kept", "overworld", 5L),
"ae2:spatial_storage", seeded("ae2:spatial_storage", "overworld", 5L),
"iristest:removed", seeded("iristest:removed", "overworld", 5L)), Map.of()));
Map<String, ModdedDimensionReplacement> recorded = ModdedReplacementReconciler.recordApplied(worldRoot,
Map.of("iristest:kept", replacement("iristest:kept", "overworld"),
"iristest:live", replacement("iristest:live", "skypack"),
"iristest:seeded", seeded("iristest:seeded", "skypack", 8L),
"iristest:wrongseed", seeded("iristest:wrongseed", "skypack", 8L),
"iristest:skipped", replacement("iristest:skipped", "skypack")),
(String level, String packRef) -> switch (level) {
case "iristest:skipped" -> OptionalLong.empty();
case "iristest:wrongseed" -> OptionalLong.of(9L);
case "iristest:seeded" -> OptionalLong.of(8L);
default -> OptionalLong.of(5L);
"iristest:wrongpack", replacement("iristest:wrongpack", "skypack"),
"iristest:absent", replacement("iristest:absent", "skypack"),
"ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld")),
(String level) -> switch (level) {
case "iristest:kept" -> Optional.of(iris("overworld", 5L));
case "iristest:live" -> Optional.of(iris("skypack", 5L));
case "iristest:seeded" -> Optional.of(iris("skypack", 8L));
case "iristest:wrongseed" -> Optional.of(iris("skypack", 9L));
case "iristest:wrongpack" -> Optional.of(iris("otherpack", 5L));
case "ae2:spatial_storage" -> Optional.of(new ModdedReplacementReconciler.LiveLevel(false, "", 0L));
default -> Optional.empty();
});
Map<String, ModdedDimensionReplacement> expected = Map.of(
"iristest:kept", seeded("iristest:kept", "overworld", 5L),
"iristest:live", seeded("iristest:live", "skypack", 5L),
"iristest:seeded", seeded("iristest:seeded", "skypack", 8L));
"iristest:seeded", seeded("iristest:seeded", "skypack", 8L),
"iristest:removed", seeded("iristest:removed", "overworld", 5L));
assertEquals(expected, recorded);
assertEquals(expected, ModdedReplacementReconciler.readState(worldRoot).applied());
} finally {
@@ -472,7 +482,55 @@ public class ModdedReplacementReconcilerTest {
assertThrows(IOException.class, () -> ModdedReplacementReconciler.recordApplied(worldRoot,
Map.of("iristest:live", replacement("iristest:live", "skypack")),
(String level, String packRef) -> OptionalLong.of(1L)));
(String level) -> Optional.of(iris("skypack", 1L))));
} finally {
deleteTree(temporary);
}
}
@Test
public void runtimeCreatedLevelIsNeverMovedRecordedOrAdopted() throws IOException {
Path temporary = Files.createTempDirectory("iris-replace-runtime-level");
Path worldRoot = temporary.resolve("world");
try {
Path spatial = worldRoot.resolve("dimensions/ae2/spatial_storage");
touch(spatial.resolve("region/r.0.0.mca"));
createHistory(temporary, spatial, ModdedWorldgenIds.dimensionTypeRef("overworld", "overworld"), "overworld",
List.of());
ModdedDimensionReplacement staged = seeded("ae2:spatial_storage", "overworld", 5L);
ModdedReplacementReconciler.stagePending(worldRoot, staged);
ModdedReplacementReconciler.Result result = reconcile(worldRoot,
Map.of("ae2:spatial_storage", staged), List.of(), temporary.resolve("config"));
assertEquals(Set.of("ae2:spatial_storage"), result.unreplaceableLevels());
assertEquals(Set.of(), result.quarantinedLevels());
assertEquals(Set.of(), result.adoptedLevels());
assertNull(result.recovery());
assertTrue(Files.isRegularFile(spatial.resolve("region/r.0.0.mca")));
assertEquals(ModdedReplacementReconciler.State.EMPTY, ModdedReplacementReconciler.readState(worldRoot));
} finally {
deleteTree(temporary);
}
}
@Test
public void levelStemOnlyDefinedByAnUnappliedIrisOverrideIsNotReplaceable() throws IOException {
Path temporary = Files.createTempDirectory("iris-replace-stale-override");
Path worldRoot = temporary.resolve("world");
try {
Path spatial = worldRoot.resolve("dimensions/ae2/spatial_storage");
touch(spatial.resolve("region/r.0.0.mca"));
ModdedDimensionReplacements.ReplaceableLevels levels = new ModdedDimensionReplacements.ReplaceableLevels(
true, Map.of("ae2:spatial_storage", ModdedDimensionReplacements.IRIS_GENERATOR), Set.of());
ModdedReplacementReconciler.Result result = ModdedReplacementReconciler.reconcile(
new ModdedReplacementReconciler.Request(worldRoot,
Map.of("ae2:spatial_storage", replacement("ae2:spatial_storage", "overworld")), List.of(),
(ModdedDimensionReplacement replacement) -> true, temporary.resolve("config"), levels));
assertEquals(Set.of("ae2:spatial_storage"), result.unreplaceableLevels());
assertTrue(Files.isRegularFile(spatial.resolve("region/r.0.0.mca")));
} finally {
deleteTree(temporary);
}
@@ -490,7 +548,7 @@ public class ModdedReplacementReconcilerTest {
IOException refused = assertThrows(IOException.class, () -> ModdedReplacementReconciler.reconcile(
new ModdedReplacementReconciler.Request(worldRoot,
Map.of("iristest:skylands", replacement("iristest:skylands", "missing")), List.of(),
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config"))));
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config"), STEMS)));
assertTrue(refused.getMessage(), refused.getMessage().contains("missing"));
assertTrue(Files.isRegularFile(region));
@@ -511,7 +569,7 @@ public class ModdedReplacementReconcilerTest {
ModdedReplacementReconciler.Result result = ModdedReplacementReconciler.reconcile(
new ModdedReplacementReconciler.Request(worldRoot,
Map.of("aether:the_aether", replacement("aether:the_aether", "missing")), List.of(),
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config")));
(ModdedDimensionReplacement replacement) -> false, temporary.resolve("config"), STEMS));
assertEquals(Set.of(), result.quarantinedLevels());
assertTrue(Files.isRegularFile(region));
@@ -543,7 +601,11 @@ public class ModdedReplacementReconcilerTest {
private static ModdedReplacementReconciler.Result reconcile(Path worldRoot, Map<String, ModdedDimensionReplacement> configured,
List<String> problems, Path recoveryParent) throws IOException {
return ModdedReplacementReconciler.reconcile(new ModdedReplacementReconciler.Request(
worldRoot, configured, problems, (ModdedDimensionReplacement replacement) -> true, recoveryParent));
worldRoot, configured, problems, (ModdedDimensionReplacement replacement) -> true, recoveryParent, STEMS));
}
private static ModdedReplacementReconciler.LiveLevel iris(String packRef, long seed) {
return new ModdedReplacementReconciler.LiveLevel(true, packRef, seed);
}
private static void writeApplied(Path worldRoot, ModdedDimensionReplacement applied) throws IOException {
@@ -0,0 +1,69 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
public class ModdedReplaceCommandsTest {
@Test
public void installedTypesListEveryPackAndItsOtherDimensionKeys() throws IOException {
Path packs = Files.createTempDirectory("iris-replace-types");
try {
touch(packs.resolve("overworld/dimensions/overworld.json"));
touch(packs.resolve("skypack/dimensions/sky.json"));
touch(packs.resolve("skypack/dimensions/floating/upper.json"));
touch(packs.resolve("empty/readme.txt"));
assertEquals(List.of("overworld", "skypack", "skypack:floating/upper", "skypack:sky"),
ModdedReplaceCommands.installedTypes(packs.toFile()));
} finally {
try (Stream<Path> walk = Files.walk(packs)) {
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
Files.deleteIfExists(path);
}
}
}
}
@Test
public void noInstalledPacksListNothing() throws IOException {
Path packs = Files.createTempDirectory("iris-replace-types-empty");
try {
assertEquals(List.of(), ModdedReplaceCommands.installedTypes(packs.toFile()));
assertEquals(List.of(), ModdedReplaceCommands.installedTypes(packs.resolve("missing").toFile()));
} finally {
Files.deleteIfExists(packs);
}
}
private static void touch(Path file) throws IOException {
Files.createDirectories(file.getParent());
Files.writeString(file, "{}", StandardCharsets.UTF_8);
}
}
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Derzeit sind kei
loaded_iris_dimensions = "Geladene Iris-Dimensionen: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Verwende /iris world create <name> <pack>, um eine Dimension ohne Neustart einzubinden."
replace_runtime_level = "{dimensionId} wird während der Laufzeit von einer Mod erstellt und ist nicht als Dimension definiert, daher kann Iris sie nicht ersetzen."
replace_registry_not_ready = "Iris liest noch die Dimensionen dieses Servers. Versuche es gleich noch einmal."
replace_installed_types = "Installierte Packs: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Externe Datapack-Importe von Modrinth herunterladen und verwalten"
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "No hay dimension
loaded_iris_dimensions = "Dimensiones de Iris cargadas: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Usa /iris world create <name> <pack> para inyectar una sin reiniciar."
replace_runtime_level = "{dimensionId} la crea un mod mientras el servidor está en marcha y no está definida como dimensión, por lo que Iris no puede reemplazarla."
replace_registry_not_ready = "Iris todavía está leyendo las dimensiones de este servidor. Inténtalo de nuevo en un momento."
replace_installed_types = "Packs instalados: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Descargar y gestionar importaciones externas de datapacks (Modrinth)"
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Ei Iris mitat ov
loaded_iris_dimensions = "Ladattu Iris mitat: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Käyttö /iris world create <name> <pack> pistääksesi yhden ilman, että aloitat alusta."
replace_runtime_level = "{dimensionId} on modin palvelimen ajon aikana luoma eikä sitä ole määritelty ulottuvuudeksi, joten Iris ei voi korvata sitä."
replace_registry_not_ready = "Iris lukee vielä tämän palvelimen ulottuvuuksia. Yritä hetken päästä uudelleen."
replace_installed_types = "Asennetut paketit: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Lataa & Hallinnoidaan ulkoisen datapaketin tuontia (Modrinth)"
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Aucune dimension
loaded_iris_dimensions = "Dimensions Iris chargées : {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Utilisez /iris world create <name> <pack> pour en injecter une sans redémarrer."
replace_runtime_level = "{dimensionId} est créée par un mod pendant que le serveur tourne et n'est pas définie comme dimension ; Iris ne peut donc pas la remplacer."
replace_registry_not_ready = "Iris lit encore les dimensions de ce serveur. Réessayez dans un instant."
replace_installed_types = "Packs installés : {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Télécharger et gérer les importations externes de datapacks (Modrinth)"
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "לא Iris המי
loaded_iris_dimensions = "טעון Iris ממדים: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "שימוש בשימוש /iris world create <name> <pack> להזריק אחד מבלי להתחיל מחדש."
replace_runtime_level = "{dimensionId} נוצר על ידי מוד בזמן שהשרת פועל ואינו מוגדר כממד, ולכן Iris אינו יכול להחליף אותו."
replace_registry_not_ready = "Iris עדיין קורא את הממדים של השרת הזה. נסה שוב בעוד רגע."
replace_installed_types = "חבילות מותקנות: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "הורד הורדה & ניהול יבוא נתונים חיצוני (Modrinth)"
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Nessuna dimensio
loaded_iris_dimensions = "Dimensioni Iris caricate: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Usa /iris world create <name> <pack> per iniettarne una senza riavviare."
replace_runtime_level = "{dimensionId} viene creata da una mod mentre il server è in esecuzione e non è definita come dimensione, quindi Iris non può sostituirla."
replace_registry_not_ready = "Iris sta ancora leggendo le dimensioni di questo server. Riprova tra un momento."
replace_installed_types = "Pack installati: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Scarica e gestisci le importazioni di datapack esterni da Modrinth"
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "現在読み込
loaded_iris_dimensions = "読み込み済み Iris ディメンション: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "再起動せずに注入するには /iris world create <name> <pack> を使用してください。"
replace_runtime_level = "{dimensionId} はサーバー実行中に Mod が作成するもので、ディメンションとして定義されていないため、Iris は置き換えられません。"
replace_registry_not_ready = "Iris はまだこのサーバーのディメンションを読み込んでいます。しばらくしてから再試行してください。"
replace_installed_types = "インストール済みのパック: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "外部データパックのインポートをダウンロードして管理します(Modrinth)"
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "현재 로드된
loaded_iris_dimensions = "로드 중 ... Iris 크기: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "재시작하지 않고 주입하려면 /iris world create <name> <pack>을(를) 사용하세요."
replace_runtime_level = "{dimensionId}은(는) 서버 실행 중에 모드가 만드는 것이며 차원으로 정의되어 있지 않으므로 Iris가 교체할 수 없습니다."
replace_registry_not_ready = "Iris가 아직 이 서버의 차원을 읽는 중입니다. 잠시 후 다시 시도하세요."
replace_installed_types = "설치된 팩: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "외부 데이터 팩 가져오기를 다운로드하고 관리합니다(Modrinth)"
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Ne Iris šiuo me
loaded_iris_dimensions = "Įkelta Iris matmenys: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Naudojimas /iris world create <name> <pack> švirkšti vieną nepradedant iš naujo."
replace_runtime_level = "{dimensionId} kuria modas serveriui veikiant, ir ji neapibrėžta kaip dimensija, todėl Iris negali jos pakeisti."
replace_registry_not_ready = "Iris vis dar skaito šio serverio dimensijas. Bandykite dar kartą po akimirkos."
replace_installed_types = "Įdiegti paketai: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Parsisiųsti & valdyti išorės importą (Modrinth)"
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Nee Iris De afme
loaded_iris_dimensions = "Geladen Iris afmetingen: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Gebruik /iris world create <name> <pack> om er één te injecteren zonder opnieuw te starten."
replace_runtime_level = "{dimensionId} wordt door een mod gemaakt terwijl de server draait en is niet als dimensie gedefinieerd, dus Iris kan haar niet vervangen."
replace_registry_not_ready = "Iris leest de dimensies van deze server nog in. Probeer het zo opnieuw."
replace_installed_types = "Geïnstalleerde packs: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Downloaden & de invoer van externe datapacks beheren (Modrinth)"
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Nie. Iris wymiar
loaded_iris_dimensions = "Załadowane Iris wymiary: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Stosowanie /iris world create <name> <pack> wykonać wstrzyknięcie bez ponownego uruchamiania."
replace_runtime_level = "{dimensionId} jest tworzony przez moda podczas działania serwera i nie jest zdefiniowany jako wymiar, więc Iris nie może go zastąpić."
replace_registry_not_ready = "Iris wciąż odczytuje wymiary tego serwera. Spróbuj ponownie za chwilę."
replace_installed_types = "Zainstalowane paczki: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Pobierz & zarządzanie importem zewnętrznych danych (Modrinth)"
@@ -1111,6 +1111,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Não Iris as dim
loaded_iris_dimensions = "Carregado Iris dimensões: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Utilização /iris world create <name> <pack> injectar um sem reiniciar."
replace_runtime_level = "{dimensionId} é criada por um mod enquanto o servidor está em execução e não está definida como dimensão, por isso o Iris não a pode substituir."
replace_registry_not_ready = "O Iris ainda está a ler as dimensões deste servidor. Tente novamente daqui a pouco."
replace_installed_types = "Packs instalados: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Baixar & gerir as importações de pacotes de dados externos (Modrinth)"
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Нет. Iris В
loaded_iris_dimensions = "загруженный Iris размеры: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Использовать /iris world create <name> <pack> Вводить один без перезапуска."
replace_runtime_level = "{dimensionId} создаётся модом во время работы сервера и не определено как измерение, поэтому Iris не может его заменить."
replace_registry_not_ready = "Iris ещё считывает измерения этого сервера. Повторите попытку чуть позже."
replace_installed_types = "Установленные паки: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "& управляет внешним импортом пакетов данных (Modrinth)"
@@ -1116,6 +1116,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Hayır Iris Boyu
loaded_iris_dimensions = "Yüklendi Iris Boyut: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Kullanım Kullanımı /iris world create <name> <pack> Yeniden başlatmadan birini enjekte etmek."
replace_runtime_level = "{dimensionId} sunucu çalışırken bir mod tarafından oluşturuluyor ve boyut olarak tanımlı değil, bu yüzden Iris onu değiştiremez."
replace_registry_not_ready = "Iris bu sunucunun boyutlarını hâlâ okuyor. Birazdan tekrar deneyin."
replace_installed_types = "Yüklü paketler: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Download indir & Dış veri paketi ithalatını yönetin (Modrinth)"
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "Không Iris Kíc
loaded_iris_dimensions = "Đã nạp Iris Kích thước: {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "Dùng /iris world create <name> <pack> tiêm một mũi mà không khởi động lại."
replace_runtime_level = "{dimensionId} được một mod tạo ra khi máy chủ đang chạy và không được định nghĩa là chiều không gian, nên Iris không thể thay thế nó."
replace_registry_not_ready = "Iris vẫn đang đọc các chiều không gian của máy chủ này. Hãy thử lại sau giây lát."
replace_installed_types = "Các gói đã cài đặt: {value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "Tải về & Quản lý các tập dữ liệu bên ngoài (Modrinth)"
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "没有 Iris 维
loaded_iris_dimensions = "已加载 Iris 维度 : {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "使用 /iris world create <name> <pack> 在不重新启动的情况下注入一个。"
replace_runtime_level = "{dimensionId} 由模组在服务器运行时创建,并未定义为维度,因此 Iris 无法替换它。"
replace_registry_not_ready = "Iris 仍在读取此服务器的维度。请稍后再试。"
replace_installed_types = "已安装的包:{value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "下载 & 管理外部数据包进口(Modrinth)"
@@ -1114,6 +1114,9 @@ no_iris_dimensions_are_currently_loaded_create_one_with_iris = "沒有 Iris 維
loaded_iris_dimensions = "已載入 Iris 維度 : {value}"
message_2 = " - {dimension}"
use_iris_world_create_name_pack_inject_one_without_restarting = "使用 /iris world create <name> <pack> 在不重新啟動的情況下注入一個。"
replace_runtime_level = "{dimensionId} 由模組在伺服器執行時建立,並未定義為維度,因此 Iris 無法取代它。"
replace_registry_not_ready = "Iris 仍在讀取此伺服器的維度。請稍後再試。"
replace_installed_types = "已安裝的包:{value}"
[iris.director.commanddatapack]
"director.download_manage_external_datapack_imports_modrinth" = "下載 & 管理外部資料包進口(Modrinth)"