This commit is contained in:
Brian Neumann-Fopiano
2026-08-12 16:31:27 -04:00
parent c59592e448
commit 13e02ecd2f
41 changed files with 352 additions and 449 deletions
+5 -4
View File
@@ -62,12 +62,13 @@ content selecting on `#minecraft:is_overworld` and friends.
## Install ## Install
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start **Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
the server. On first boot Iris downloads the managed `overworld` and `underworld` beta packs automatically. the server. First boot performs no pack download. Run `/iris download overworld`,
`/iris download underworld`, or `/iris download https://host/path/pack.zip`, then restart manually.
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is **Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
self-contained (core, SPI, and required Fabric API modules are bundled). On first boot Iris self-contained (core, SPI, and required Fabric API modules are bundled). First boot compiles only
prefetches the managed `overworld` and `underworld` beta packs and rebuilds the worldgen datapack; packs already on disk and never accesses the network. `/iris download` installs a pack atomically
restart once if startup reports that registry-visible pack data was installed too late for that boot. Packs installed later register their custom dimension types without stopping the server; restart manually afterward. Packs register their custom dimension types
(height ranges) and custom biomes through the forced datapack at server start - restart once after (height ranges) and custom biomes through the forced datapack at server start - restart once after
adding a pack so worlds get its full heights and biomes; worlds created before that restart run adding a pack so worlds get its full heights and biomes; worlds created before that restart run
with fallback heights. with fallback heights.
@@ -34,9 +34,7 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin; import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.NonNull; import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator; import org.bukkit.generator.ChunkGenerator;
@@ -177,13 +175,8 @@ public final class IrisWorldGeneratorResolver {
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder."); + " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
return null; return null;
} }
Iris.warn("Unable to find dimension type " + id + " Looking for online packs..."); Iris.warn("Unable to find dimension type " + id + ". Install its pack with /iris download "
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); + id + " and restart the server.");
dimension = IrisData.loadAnyDimension(id, null);
if (dimension != null) {
Iris.info("Resolved missing dimension, proceeding.");
}
} }
return dimension; return dimension;
@@ -710,9 +710,14 @@ public class CommandIris implements DirectorExecutor {
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", descriptionKey = "iris.director.commandiris.param.whether_not_overwrite_pack_with_downloaded_one", aliases = "force", defaultValue = "false") @Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", descriptionKey = "iris.director.commandiris.param.whether_not_overwrite_pack_with_downloaded_one", aliases = "force", defaultValue = "false")
boolean overwrite boolean overwrite
) { ) {
if (PackDownloader.isManagedBetaPack(pack)) { if (PackDownloader.isDirectZipUrl(pack)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOWNLOADING_PACK_BETA_RELEASE, MessageArgument.untrusted("pack", pack), MessageArgument.trusted("value", overwrite ? IrisLanguage.text(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX) : ""))); sender().sendMessage("Downloading Iris pack from " + pack
Iris.service(StudioSVC.class).downloadManagedBeta(sender(), pack, overwrite); + (overwrite ? IrisLanguage.text(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX) : "") + ".");
Iris.service(StudioSVC.class).downloadUrl(sender(), pack, overwrite);
} else if (PackDownloader.isManagedPack(pack)) {
sender().sendMessage("Downloading managed Iris pack '" + pack + "' from its configured Git source"
+ (overwrite ? IrisLanguage.text(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX) : "") + ".");
Iris.service(StudioSVC.class).downloadManaged(sender(), pack, overwrite);
} else { } else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOWNLOADING_PACK, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("branch", branch), MessageArgument.trusted("value", overwrite ? IrisLanguage.text(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX) : ""))); sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOWNLOADING_PACK, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("branch", branch), MessageArgument.trusted("value", overwrite ? IrisLanguage.text(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX) : "")));
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite); Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
@@ -260,7 +260,7 @@ public final class ModdedEngineBootstrap {
selfTest(moddedLoader.getClass().getClassLoader()); selfTest(moddedLoader.getClass().getClassLoader());
bind(); bind();
IrisLanguage.initialize(); IrisLanguage.initialize();
ModdedStartup.prefetchDefaultPack(); ModdedStartup.prefetchStartupDatapack();
if (!moddedLoader.clientEnvironment()) { if (!moddedLoader.clientEnvironment()) {
MainWorldService.reconcileEarly(); MainWorldService.reconcileEarly();
} }
@@ -253,8 +253,6 @@ public final class ModdedForcedDatapack {
} }
private static Path write() throws IOException { private static Path write() throws IOException {
// No ensureDefaultPack() here: write() is reachable from loadPacks on the first boot, and loadPacks
// must never touch the network. ModdedStartup.prefetchDefaultPack covers the download at boot.
String packsHash = packsHash(); String packsHash = packsHash();
Path datapackRoot = datapackRoot(); Path datapackRoot = datapackRoot();
Files.createDirectories(datapackRoot); Files.createDirectories(datapackRoot);
@@ -33,17 +33,15 @@ public final class ModdedModConfig {
private static volatile ModdedModConfig instance; private static volatile ModdedModConfig instance;
private final String defaultPack; private final String defaultPack;
private final boolean autoDownloadDefaultPack;
private final String primaryWorld; private final String primaryWorld;
private final boolean routePlayersToPrimaryWorld; private final boolean routePlayersToPrimaryWorld;
private final String mainWorldPack; private final String mainWorldPack;
private final long mainWorldSeed; private final long mainWorldSeed;
private final boolean mainWorldAutoRestart; private final boolean mainWorldAutoRestart;
private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld, private ModdedModConfig(String defaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld,
String mainWorldPack, long mainWorldSeed, boolean mainWorldAutoRestart) { String mainWorldPack, long mainWorldSeed, boolean mainWorldAutoRestart) {
this.defaultPack = defaultPack; this.defaultPack = defaultPack;
this.autoDownloadDefaultPack = autoDownloadDefaultPack;
this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim(); this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim();
this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld; this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld;
this.mainWorldPack = mainWorldPack == null ? "" : mainWorldPack.trim(); this.mainWorldPack = mainWorldPack == null ? "" : mainWorldPack.trim();
@@ -68,7 +66,7 @@ public final class ModdedModConfig {
public static void setPrimaryWorld(String dimensionId) { public static void setPrimaryWorld(String dimensionId) {
synchronized (LOCK) { synchronized (LOCK) {
ModdedModConfig current = get(); ModdedModConfig current = get();
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld, ModdedModConfig updated = new ModdedModConfig(current.defaultPack, dimensionId, current.routePlayersToPrimaryWorld,
current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart); current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart);
instance = updated; instance = updated;
write(configFile(), updated); write(configFile(), updated);
@@ -78,7 +76,7 @@ public final class ModdedModConfig {
public static void setMainWorld(String packRef, long seed) { public static void setMainWorld(String packRef, long seed) {
synchronized (LOCK) { synchronized (LOCK) {
ModdedModConfig current = get(); ModdedModConfig current = get();
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld, ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld,
packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart); packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart);
instance = updated; instance = updated;
write(configFile(), updated); write(configFile(), updated);
@@ -89,10 +87,6 @@ public final class ModdedModConfig {
return defaultPack; return defaultPack;
} }
public boolean autoDownloadDefaultPack() {
return autoDownloadDefaultPack;
}
public String primaryWorld() { public String primaryWorld() {
return primaryWorld; return primaryWorld;
} }
@@ -119,7 +113,7 @@ public final class ModdedModConfig {
private static ModdedModConfig load() { private static ModdedModConfig load() {
Path file = configFile(); Path file = configFile();
ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true, "", 0L, false); ModdedModConfig defaults = new ModdedModConfig("overworld", "", true, "", 0L, false);
if (!Files.isRegularFile(file)) { if (!Files.isRegularFile(file)) {
write(file, defaults); write(file, defaults);
return defaults; return defaults;
@@ -128,7 +122,6 @@ public final class ModdedModConfig {
JSONObject json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8)); JSONObject json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
return new ModdedModConfig( return new ModdedModConfig(
json.optString("defaultPack", defaults.defaultPack), json.optString("defaultPack", defaults.defaultPack),
json.optBoolean("autoDownloadDefaultPack", defaults.autoDownloadDefaultPack),
json.optString("primaryWorld", defaults.primaryWorld), json.optString("primaryWorld", defaults.primaryWorld),
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld), json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld),
json.optString("mainWorldPack", defaults.mainWorldPack), json.optString("mainWorldPack", defaults.mainWorldPack),
@@ -143,7 +136,6 @@ public final class ModdedModConfig {
private static void write(Path file, ModdedModConfig config) { private static void write(Path file, ModdedModConfig config) {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("defaultPack", config.defaultPack); json.put("defaultPack", config.defaultPack);
json.put("autoDownloadDefaultPack", config.autoDownloadDefaultPack);
json.put("primaryWorld", config.primaryWorld); json.put("primaryWorld", config.primaryWorld);
json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld); json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld);
json.put("mainWorldPack", config.mainWorldPack); json.put("mainWorldPack", config.mainWorldPack);
@@ -48,7 +48,7 @@ public final class ModdedPackInstaller {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.INVALID_PACK_NAME, MessageArgument.untrusted("pack", String.valueOf(pack)))); feedback.accept(IrisLanguage.plain(PackDownloadMessages.INVALID_PACK_NAME, MessageArgument.untrusted("pack", String.valueOf(pack))));
return false; return false;
} }
boolean managedBeta = PackDownloader.isManagedBetaPack(pack); boolean managed = PackDownloader.isManagedPack(pack);
if (!acceptsBranch(pack, branch)) { if (!acceptsBranch(pack, branch)) {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.INVALID_BRANCH_NAME, MessageArgument.untrusted("branch", String.valueOf(branch)))); feedback.accept(IrisLanguage.plain(PackDownloadMessages.INVALID_BRANCH_NAME, MessageArgument.untrusted("branch", String.valueOf(branch))));
return false; return false;
@@ -58,8 +58,8 @@ public final class ModdedPackInstaller {
synchronized (installLock) { synchronized (installLock) {
File packs = configDir.resolve("irisworldgen").resolve("packs").toFile(); File packs = configDir.resolve("irisworldgen").resolve("packs").toFile();
try { try {
PackDownloader.PackInstallResult result = managedBeta PackDownloader.PackInstallResult result = managed
? PackDownloader.downloadManagedBeta(packs, pack, forceOverwrite, feedback) ? PackDownloader.downloadManaged(packs, pack, forceOverwrite, feedback)
: PackDownloader.download( : PackDownloader.download(
packs, packs,
"IrisDimensions/" + pack, "IrisDimensions/" + pack,
@@ -84,7 +84,7 @@ public final class ModdedPackInstaller {
} }
return installed; return installed;
} catch (IOException error) { } catch (IOException error) {
String source = managedBeta ? "beta release" : "branch " + branch; String source = managed ? "configured Git source" : "branch " + branch;
LOGGER.error("Iris pack download failed for IrisDimensions/{} ({})", pack, source, error); LOGGER.error("Iris pack download failed for IrisDimensions/{} ({})", pack, source, error);
feedback.accept(IrisLanguage.plain( feedback.accept(IrisLanguage.plain(
PackDownloadMessages.DOWNLOAD_FAILED, PackDownloadMessages.DOWNLOAD_FAILED,
@@ -97,7 +97,7 @@ public final class ModdedPackInstaller {
} }
static boolean acceptsBranch(String pack, String branch) { static boolean acceptsBranch(String pack, String branch) {
return PackDownloader.isManagedBetaPack(pack) return PackDownloader.isManagedPack(pack)
|| (branch != null && BRANCH_NAME.matcher(branch).matches()); || (branch != null && BRANCH_NAME.matcher(branch).matches());
} }
@@ -37,7 +37,6 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -47,7 +46,6 @@ public final class ModdedStartup {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final AtomicBoolean PREPARED = new AtomicBoolean(false); private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
private static final AtomicBoolean STARTED = new AtomicBoolean(false); private static final AtomicBoolean STARTED = new AtomicBoolean(false);
private static final Object PACK_LOCK = new Object();
private ModdedStartup() { private ModdedStartup() {
} }
@@ -69,14 +67,13 @@ public final class ModdedStartup {
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this * ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
* one-shot task, and the datapack must be regenerated before the level PackRepository reload if it can. * one-shot task, and the datapack must be regenerated before the level PackRepository reload if it can.
*/ */
public static void prefetchDefaultPack() { public static void prefetchStartupDatapack() {
Thread thread = new Thread(ModdedStartup::refreshPacksAndDatapack, "iris-modded-pack-prefetch"); Thread thread = new Thread(ModdedStartup::refreshDatapack, "iris-modded-datapack-prefetch");
thread.setDaemon(true); thread.setDaemon(true);
thread.start(); thread.start();
} }
private static void refreshPacksAndDatapack() { private static void refreshDatapack() {
ensureDefaultPack();
try { try {
ModdedForcedDatapack.regenerateIfStale("boot"); ModdedForcedDatapack.regenerateIfStale("boot");
} catch (Throwable failure) { } catch (Throwable failure) {
@@ -98,10 +95,10 @@ public final class ModdedStartup {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull(); ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) { if (scheduler == null) {
refreshPacksAndDatapack(); refreshDatapack();
return; return;
} }
scheduler.async(ModdedStartup::refreshPacksAndDatapack); scheduler.async(ModdedStartup::refreshDatapack);
} }
public static void validateAllPacks() { public static void validateAllPacks() {
@@ -248,45 +245,4 @@ public final class ModdedStartup {
} }
} }
public static void ensureDefaultPack() {
synchronized (PACK_LOCK) {
ModdedModConfig config = ModdedModConfig.get();
if (!config.autoDownloadDefaultPack()) {
return;
}
Path configDir = ModdedEngineBootstrap.loader().configDir();
File packsRoot = ModdedPackCommands.packsRoot();
for (String pack : startupPacks(config.defaultPack())) {
ensureStartupPack(configDir, packsRoot, pack);
}
}
}
static List<String> startupPacks(String configuredDefault) {
LinkedHashSet<String> packs = new LinkedHashSet<>(PackDownloader.managedBetaPacks());
if (configuredDefault != null && !configuredDefault.isBlank()) {
packs.add(configuredDefault);
}
return List.copyOf(packs);
}
private static void ensureStartupPack(Path configDir, File packsRoot, String pack) {
boolean present = PackDownloader.isManagedBetaPack(pack)
? PackDownloader.isManagedBetaPackPresent(packsRoot, pack)
: PackDownloader.isPackPresent(packsRoot, pack);
if (present) {
return;
}
boolean managedBeta = PackDownloader.isManagedBetaPack(pack);
String branch = managedBeta ? "beta" : "master";
String source = managedBeta ? "beta release" : "master branch";
String role = managedBeta ? "managed beta pack" : "default pack";
LOGGER.info("Iris {} '{}' missing; downloading IrisDimensions/{} ({})", role, pack, pack, source);
boolean installed = ModdedPackInstaller.install(
configDir, pack, branch, false, false,
(String line) -> LOGGER.info("Iris: {}", line));
if (!installed) {
LOGGER.warn("Iris {} '{}' could not be downloaded; install it with /iris download {}", role, pack, pack);
}
}
} }
@@ -49,6 +49,8 @@ import net.minecraft.world.level.chunk.ChunkGenerator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -258,8 +260,10 @@ public final class IrisModdedCommands {
static int download(CommandSourceStack source, String pack, static int download(CommandSourceStack source, String pack,
String branch, boolean forceOverwrite) { String branch, boolean forceOverwrite) {
boolean managedBeta = PackDownloader.isManagedBetaPack(pack); boolean managed = PackDownloader.isManagedPack(pack);
String baseDownloadSource = managedBeta ? "beta release" : "branch " + branch; boolean directUrl = PackDownloader.isDirectZipUrl(pack);
String baseDownloadSource = directUrl ? "direct ZIP URL"
: managed ? "configured Git source" : "branch " + branch;
String downloadSource = forceOverwrite String downloadSource = forceOverwrite
? baseDownloadSource + IrisLanguage.plain(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX) ? baseDownloadSource + IrisLanguage.plain(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX)
: baseDownloadSource; : baseDownloadSource;
@@ -273,11 +277,28 @@ public final class IrisModdedCommands {
return 0; return 0;
} }
scheduler.async(() -> { scheduler.async(() -> {
boolean installed = ModdedPackInstaller.install( boolean installed;
ModdedEngineBootstrap.loader().configDir(), pack, branch, forceOverwrite, true, if (directUrl) {
(String message) -> scheduler.global(() -> ok(source, message))); File packs = ModdedPackCommands.packsRoot();
try {
PackDownloader.PackInstallResult result = PackDownloader.downloadUrl(
packs,
pack,
forceOverwrite,
(String message) -> scheduler.global(() -> ok(source, message))
);
installed = result != null;
} catch (IOException error) {
LOGGER.error("Iris pack download failed for direct URL {}", pack, error);
installed = false;
}
} else {
installed = ModdedPackInstaller.install(
ModdedEngineBootstrap.loader().configDir(), pack, branch, forceOverwrite, false,
(String message) -> scheduler.global(() -> ok(source, message)));
}
if (installed) { if (installed) {
scheduler.global(() -> ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_INSTALLED_ITS_EXACT_DIMENSION_TYPES_CUSTOM_BIOMES_JOIN_FORCED, MessageArgument.untrusted("pack", pack)))); scheduler.global(() -> ok(source, "Pack installed on disk. Restart the server before using it."));
} else { } else {
scheduler.global(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("downloadSource", downloadSource)))); scheduler.global(() -> fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("downloadSource", downloadSource))));
} }
@@ -26,7 +26,7 @@ import static org.junit.Assert.assertTrue;
public class ModdedPackInstallerTest { public class ModdedPackInstallerTest {
@Test @Test
public void managedBetaPacksIgnoreTheRequestedBranch() { public void managedPacksIgnoreTheRequestedBranch() {
assertTrue(ModdedPackInstaller.acceptsBranch("overworld", null)); assertTrue(ModdedPackInstaller.acceptsBranch("overworld", null));
assertTrue(ModdedPackInstaller.acceptsBranch("underworld", "feature/arbitrary")); assertTrue(ModdedPackInstaller.acceptsBranch("underworld", "feature/arbitrary"));
} }
@@ -1,41 +0,0 @@
/*
* 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;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class ModdedStartupPackSelectionTest {
@Test
public void managedBetaPacksAreAlwaysInstalledInStableOrder() {
assertEquals(List.of("overworld", "underworld"), ModdedStartup.startupPacks("overworld"));
assertEquals(List.of("overworld", "underworld"), ModdedStartup.startupPacks("underworld"));
}
@Test
public void configuredNonManagedDefaultIsInstalledAfterManagedBetaPacks() {
assertEquals(
List.of("overworld", "underworld", "custom"),
ModdedStartup.startupPacks("custom")
);
}
}
@@ -151,7 +151,6 @@ public class IrisSettings {
public static class IrisSettingsAutoconfiguration { public static class IrisSettingsAutoconfiguration {
public boolean configureSpigotTimeoutTime = true; public boolean configureSpigotTimeoutTime = true;
public boolean configurePaperWatchdogDelay = true; public boolean configurePaperWatchdogDelay = true;
public boolean autoRestartOnCustomBiomeInstall = true;
} }
@Data @Data
@@ -7,16 +7,13 @@ import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.misc.ServerProperties; import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.io.IO; 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.Bukkit;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@@ -231,12 +228,8 @@ public class IrisWorlds {
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder."); + " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
return null; return null;
} }
IrisLogging.warn("Unable to find dimension type " + id + " Looking for online packs..."); IrisLogging.warn("Unable to find dimension type " + id + ". Install it with /iris download "
IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false); + id + " and restart the server.");
dimension = IrisData.loadAnyDimension(id, null);
if (dimension != null) {
IrisLogging.info("Resolved missing dimension, proceeding.");
}
} }
return dimension; return dimension;
} }
@@ -114,8 +114,8 @@ public class ServerConfigurator {
loadedDatapackRuntimeReady = result.succeeded() loadedDatapackRuntimeReady = result.succeeded()
&& !result.restartRequired() && !result.restartRequired()
&& pinLoadedDatapackCompilerInputs(); && pinLoadedDatapackCompilerInputs();
if (result.restartRequired() && IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall()) { if (result.restartRequired()) {
restart(); IrisLogging.warn("Iris datapack changes require another server restart before worlds can use them.");
} }
} }
} }
@@ -295,10 +295,6 @@ public final class BukkitCommandMessagesExtended {
"iris.bukkit.commandiris.set_debug", "iris.bukkit.commandiris.set_debug",
C.GREEN + "Set debug to: " + "{to}" C.GREEN + "Set debug to: " + "{to}"
); );
public static final TextKey COMMAND_IRIS_DOWNLOADING_PACK_BETA_RELEASE = TextKey.of(
"iris.bukkit.commandiris.downloading_pack_beta_release",
C.GREEN + "Downloading pack: " + "{pack}" + " (beta release)" + "{value}"
);
public static final TextKey COMMAND_IRIS_DOWNLOADING_PACK = TextKey.of( public static final TextKey COMMAND_IRIS_DOWNLOADING_PACK = TextKey.of(
"iris.bukkit.commandiris.downloading_pack", "iris.bukkit.commandiris.downloading_pack",
C.GREEN + "Downloading pack: " + "{pack}" + "/" + "{branch}" + "{value}" C.GREEN + "Downloading pack: " + "{pack}" + "/" + "{branch}" + "{value}"
@@ -916,7 +912,6 @@ public final class BukkitCommandMessagesExtended {
COMMAND_IRIS_LOOKS_LIKE_WORLD_WAS_ALREADY_REMOVED_FROM_BUKKIT_YML, COMMAND_IRIS_LOOKS_LIKE_WORLD_WAS_ALREADY_REMOVED_FROM_BUKKIT_YML,
COMMAND_IRIS_FAILED_SAVE_BUKKIT_YML_BECAUSE, COMMAND_IRIS_FAILED_SAVE_BUKKIT_YML_BECAUSE,
COMMAND_IRIS_SET_DEBUG, COMMAND_IRIS_SET_DEBUG,
COMMAND_IRIS_DOWNLOADING_PACK_BETA_RELEASE,
COMMAND_IRIS_DOWNLOADING_PACK, COMMAND_IRIS_DOWNLOADING_PACK,
COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD, COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD,
COMMAND_IRIS_SENDING_METRICS, COMMAND_IRIS_SENDING_METRICS,
@@ -446,10 +446,6 @@ public final class BukkitRuntimeMessages {
"iris.bukkit.runtime.studiosvc.failed_download", "iris.bukkit.runtime.studiosvc.failed_download",
"Failed to download '" + "{key}" + "'." "Failed to download '" + "{key}" + "'."
); );
public static final TextKey STUDIO_S_V_C_FAILED_DOWNLOAD_IRISDIMENSIONS_OVERWORLD_BETA_RELEASE = TextKey.of(
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release",
"Failed to download the IrisDimensions/overworld beta release."
);
public static final TextKey STUDIO_S_V_C_FAILED_DOWNLOAD_BRANCH = TextKey.of( public static final TextKey STUDIO_S_V_C_FAILED_DOWNLOAD_BRANCH = TextKey.of(
"iris.bukkit.runtime.studiosvc.failed_download_branch", "iris.bukkit.runtime.studiosvc.failed_download_branch",
"Failed to download '" + "{repo}" + "' (branch " + "{branch}" + ")." "Failed to download '" + "{repo}" + "' (branch " + "{branch}" + ")."
@@ -730,7 +726,6 @@ public final class BukkitRuntimeMessages {
STUDIO_S_V_C_PACK_WAS_NOT_FOUND_PACK_LISTING, STUDIO_S_V_C_PACK_WAS_NOT_FOUND_PACK_LISTING,
STUDIO_S_V_C_USE_IRIS_DOWNLOAD_PACK_BRANCH_BRANCH_DOWNLOAD_MANUALLY, STUDIO_S_V_C_USE_IRIS_DOWNLOAD_PACK_BRANCH_BRANCH_DOWNLOAD_MANUALLY,
STUDIO_S_V_C_FAILED_DOWNLOAD, STUDIO_S_V_C_FAILED_DOWNLOAD,
STUDIO_S_V_C_FAILED_DOWNLOAD_IRISDIMENSIONS_OVERWORLD_BETA_RELEASE,
STUDIO_S_V_C_FAILED_DOWNLOAD_BRANCH, STUDIO_S_V_C_FAILED_DOWNLOAD_BRANCH,
STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD, STUDIO_S_V_C_FAILED_OPEN_STUDIO_WORLD,
STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS, STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS,
@@ -43,20 +43,9 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; 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();
new PackSpec(
"overworld",
URI.create("https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip"),
"overworld"
),
new PackSpec(
"underworld",
URI.create("https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip"),
"underworld"
)
);
private static final String WORLD_DATAPACK_DIRECTORY = "iris"; private static final String WORLD_DATAPACK_DIRECTORY = "iris";
private static final int MARKER_SCHEMA = 4; private static final int MARKER_SCHEMA = 5;
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;
@@ -239,9 +228,6 @@ public final class DefaultPackBootstrapProvisioner {
} }
List<File> packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot()); List<File> packRoots = IrisDatapackCompiler.collectPackRoots(normalizedData, options.levelRoot());
if (packRoots.isEmpty()) {
throw new IOException("No Iris pack roots were available for bootstrap datapack compilation");
}
IDataFixer fixer = DataVersion.getLatest().get(); IDataFixer fixer = DataVersion.getLatest().get();
if (fixer == null) { if (fixer == null) {
throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap"); throw new IOException("Latest Iris datapack fixer is unavailable during bootstrap");
@@ -260,7 +246,7 @@ public final class DefaultPackBootstrapProvisioner {
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, fixer, false);
if (!isDatapackRoot(compileContainer)) { 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);
} }
datapackBackup = replaceWithBackup(compileContainer, datapackRoot); datapackBackup = replaceWithBackup(compileContainer, datapackRoot);
@@ -271,12 +257,12 @@ public final class DefaultPackBootstrapProvisioner {
for (PackPlan plan : plans) { for (PackPlan plan : plans) {
validatePackRoot(plan.root(), plan.spec()); validatePackRoot(plan.root(), plan.spec());
} }
if (!isDatapackRoot(datapackRoot)) {
throw new IOException("Bootstrap datapack output is incomplete at " + datapackRoot);
}
List<File> finalPackRoots = IrisDatapackCompiler.collectPackRoots( List<File> finalPackRoots = IrisDatapackCompiler.collectPackRoots(
normalizedData, normalizedData,
options.levelRoot()); options.levelRoot());
if (!isDatapackRoot(datapackRoot, !finalPackRoots.isEmpty())) {
throw new IOException("Bootstrap datapack output is incomplete at " + datapackRoot);
}
String finalAggregateFingerprint = packRootsFingerprint(finalPackRoots); String finalAggregateFingerprint = packRootsFingerprint(finalPackRoots);
String finalCompilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint( String finalCompilerInputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
finalPackRoots, finalPackRoots,
@@ -316,7 +302,6 @@ public final class DefaultPackBootstrapProvisioner {
} else { } else {
status = ProvisionStatus.UNCHANGED; status = ProvisionStatus.UNCHANGED;
} }
feedback.accept("Iris bootstrap packs are " + status.name().toLowerCase() + ".");
Map<String, Path> provisionedPacks = new LinkedHashMap<>(); Map<String, Path> provisionedPacks = new LinkedHashMap<>();
for (PackPlan plan : plans) { for (PackPlan plan : plans) {
provisionedPacks.put(plan.spec().key(), plan.root()); provisionedPacks.put(plan.spec().key(), plan.root());
@@ -605,9 +590,13 @@ public final class DefaultPackBootstrapProvisioner {
} }
private static boolean isDatapackRoot(Path path) { private static boolean isDatapackRoot(Path path) {
return isDatapackRoot(path, false);
}
private static boolean isDatapackRoot(Path path, boolean requiresGeneratedTypes) {
return path != null return path != null
&& Files.isRegularFile(path.resolve("pack.mcmeta")) && Files.isRegularFile(path.resolve("pack.mcmeta"))
&& Files.isDirectory(path.resolve("data/iris/dimension_type")); && (!requiresGeneratedTypes || Files.isDirectory(path.resolve("data/iris/dimension_type")));
} }
static Path worldDatapackRoot(Path levelRoot) { static Path worldDatapackRoot(Path levelRoot) {
@@ -908,7 +897,7 @@ public final class DefaultPackBootstrapProvisioner {
Objects.requireNonNull(requestTimeout, "requestTimeout"); Objects.requireNonNull(requestTimeout, "requestTimeout");
Objects.requireNonNull(retryDelay, "retryDelay"); Objects.requireNonNull(retryDelay, "retryDelay");
Objects.requireNonNull(levelRoot, "levelRoot"); Objects.requireNonNull(levelRoot, "levelRoot");
if (packs.isEmpty() || attempts < 1 || maxArchiveBytes < 1L) { if (attempts < 1 || maxArchiveBytes < 1L) {
throw new IllegalArgumentException("Invalid bootstrap provisioning options"); throw new IllegalArgumentException("Invalid bootstrap provisioning options");
} }
Set<String> keys = new HashSet<>(); Set<String> keys = new HashSet<>();
@@ -32,6 +32,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files; 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;
@@ -56,14 +57,14 @@ import java.util.zip.ZipInputStream;
public final class PackDownloader { public final class PackDownloader {
private static final String DEFAULT_OVERWORLD_PACK = "overworld"; private static final String DEFAULT_OVERWORLD_PACK = "overworld";
private static final String DEFAULT_OVERWORLD_REPOSITORY = "IrisDimensions/overworld"; private static final String DEFAULT_OVERWORLD_REPOSITORY = "IrisDimensions/overworld";
private static final String DEFAULT_OVERWORLD_RELEASE_URL = "https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip"; private static final String DEFAULT_OVERWORLD_REF = "master";
private static final String UNDERWORLD_PACK = "underworld"; private static final String UNDERWORLD_PACK = "underworld";
private static final String UNDERWORLD_REPOSITORY = "IrisDimensions/underworld"; private static final String UNDERWORLD_REPOSITORY = "IrisDimensions/underworld";
private static final String UNDERWORLD_RELEASE_URL = "https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip"; private static final String UNDERWORLD_REF = "main";
private static final List<String> MANAGED_BETA_PACK_KEYS = List.of(DEFAULT_OVERWORLD_PACK, UNDERWORLD_PACK); private static final List<String> MANAGED_PACK_KEYS = List.of(DEFAULT_OVERWORLD_PACK, UNDERWORLD_PACK);
private static final Map<String, ManagedBetaPack> MANAGED_BETA_PACKS = Map.of( private static final Map<String, ManagedPack> MANAGED_PACKS = Map.of(
DEFAULT_OVERWORLD_PACK, new ManagedBetaPack(DEFAULT_OVERWORLD_REPOSITORY, DEFAULT_OVERWORLD_RELEASE_URL), DEFAULT_OVERWORLD_PACK, new ManagedPack(DEFAULT_OVERWORLD_REPOSITORY, DEFAULT_OVERWORLD_REF),
UNDERWORLD_PACK, new ManagedBetaPack(UNDERWORLD_REPOSITORY, UNDERWORLD_RELEASE_URL) UNDERWORLD_PACK, new ManagedPack(UNDERWORLD_REPOSITORY, UNDERWORLD_REF)
); );
private static final Pattern GITHUB_REPOSITORY = Pattern.compile("[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+"); private static final Pattern GITHUB_REPOSITORY = Pattern.compile("[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+");
private static final Pattern GITHUB_REF = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._/-]*"); private static final Pattern GITHUB_REF = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._/-]*");
@@ -84,12 +85,30 @@ public final class PackDownloader {
return DEFAULT_OVERWORLD_PACK.equals(pack); return DEFAULT_OVERWORLD_PACK.equals(pack);
} }
public static boolean isManagedBetaPack(String pack) { public static boolean isManagedPack(String pack) {
return pack != null && MANAGED_BETA_PACKS.containsKey(pack); return pack != null && MANAGED_PACKS.containsKey(pack);
} }
public static List<String> managedBetaPacks() { public static List<String> managedPacks() {
return MANAGED_BETA_PACK_KEYS; return MANAGED_PACK_KEYS;
}
public static boolean isDirectZipUrl(String value) {
if (value == null || value.isBlank()) {
return false;
}
try {
URI uri = URI.create(value.trim());
String scheme = uri.getScheme();
String path = uri.getPath();
return ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme))
&& uri.getHost() != null
&& !uri.getHost().isBlank()
&& path != null
&& path.toLowerCase(Locale.ROOT).endsWith(".zip");
} catch (IllegalArgumentException exception) {
return false;
}
} }
public static String defaultOverworldPack() { public static String defaultOverworldPack() {
@@ -133,8 +152,8 @@ public final class PackDownloader {
} }
} }
public static boolean isManagedBetaPackPresent(File packsFolder, String key) { public static boolean isManagedPackPresent(File packsFolder, String key) {
if (!isManagedBetaPack(key) || !isPackPresent(packsFolder, key)) { if (!isManagedPack(key) || !isPackPresent(packsFolder, key)) {
return false; return false;
} }
File resolvedPack = PackDirectoryResolver.resolveExisting(packsFolder, key); File resolvedPack = PackDirectoryResolver.resolveExisting(packsFolder, key);
@@ -149,32 +168,46 @@ public final class PackDownloader {
} }
public static PackInstallResult downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer<String> feedback) throws IOException { public static PackInstallResult downloadDefaultOverworld(File packsFolder, boolean forceOverwrite, Consumer<String> feedback) throws IOException {
return downloadManagedBeta(packsFolder, DEFAULT_OVERWORLD_PACK, forceOverwrite, feedback); return downloadManaged(packsFolder, DEFAULT_OVERWORLD_PACK, forceOverwrite, feedback);
} }
public static PackInstallResult downloadManagedBeta(File packsFolder, String pack, boolean forceOverwrite, public static PackInstallResult downloadManaged(File packsFolder, String pack, boolean forceOverwrite,
Consumer<String> feedback) throws IOException { Consumer<String> feedback) throws IOException {
ManagedBetaPack managed = pack == null ? null : MANAGED_BETA_PACKS.get(pack); ManagedPack managed = pack == null ? null : MANAGED_PACKS.get(pack);
if (managed == null) { if (managed == null) {
throw new IllegalArgumentException("Pack '" + pack + "' has no managed beta release"); throw new IllegalArgumentException("Pack '" + pack + "' has no managed Git source");
} }
return download( return download(
packsFolder, packsFolder,
managed.repository(), managed.repository(),
managed.releaseUrl(), managed.ref(),
forceOverwrite,
false,
pack,
feedback
);
}
public static PackInstallResult downloadUrl(File packsFolder, String url, boolean forceOverwrite,
Consumer<String> feedback) throws IOException {
if (!isDirectZipUrl(url)) {
throw new IllegalArgumentException("Pack URL must be an HTTP or HTTPS .zip link");
}
return download(
packsFolder,
"direct-url",
url.trim(),
forceOverwrite, forceOverwrite,
true, true,
pack, null,
feedback feedback
); );
} }
/** /**
* Downloads and imports a pack. {@code expectedKey} is the pack key the caller is trying to * Downloads and imports a pack. {@code expectedKey} is the pack key the caller is trying to
* obtain (null when unknown, e.g. arbitrary repo/branch downloads); when the key is already * obtain (null when unknown, e.g. arbitrary repo, branch, or URL downloads); when the key is
* present on disk and {@code forceOverwrite} is false, the network is never touched. The * already present on disk and {@code forceOverwrite} is false, the network is never touched.
* per-repo lock keeps concurrent startup triggers (async default-pack install racing world
* resolution) from downloading the same archive twice.
*/ */
public static PackInstallResult download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, String expectedKey, Consumer<String> feedback) throws IOException { public static PackInstallResult download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, String expectedKey, Consumer<String> feedback) throws IOException {
Objects.requireNonNull(packsFolder, "packsFolder"); Objects.requireNonNull(packsFolder, "packsFolder");
@@ -185,8 +218,8 @@ public final class PackDownloader {
} }
String lockKey = expectedKey != null && !expectedKey.isBlank() ? "key:" + expectedKey : "ref:" + repo + "|" + ref; String lockKey = expectedKey != null && !expectedKey.isBlank() ? "key:" + expectedKey : "ref:" + repo + "|" + ref;
return withDownloadLock(lockKey, () -> { return withDownloadLock(lockKey, () -> {
boolean present = isManagedBetaPack(expectedKey) boolean present = isManagedPack(expectedKey)
? isManagedBetaPackPresent(packsFolder, expectedKey) ? isManagedPackPresent(packsFolder, expectedKey)
: isPackPresent(packsFolder, expectedKey); : isPackPresent(packsFolder, expectedKey);
if (!forceOverwrite && present) { if (!forceOverwrite && present) {
sendFeedback(output, IrisLanguage.plain(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey))); sendFeedback(output, IrisLanguage.plain(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey)));
@@ -224,14 +257,7 @@ public final class PackDownloader {
sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.NO_EXTRACTED_FILES)); sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.NO_EXTRACTED_FILES));
return null; return null;
} }
File directory; File directory = findExtractedPackDirectory(work, zipFiles);
try {
directory = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null;
} catch (NullPointerException exception) {
IrisLogging.reportError(exception);
sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.HOME_DIRECTORY_ERROR));
return null;
}
if (directory == null) { if (directory == null) {
sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.INVALID_ARCHIVE_FORMAT)); sendFeedback(feedback, IrisLanguage.plain(PackDownloadMessages.INVALID_ARCHIVE_FORMAT));
return null; return null;
@@ -242,6 +268,26 @@ public final class PackDownloader {
} }
} }
private static File findExtractedPackDirectory(File work, File[] extractedFiles) {
if (new File(work, "dimensions").isDirectory()) {
return work;
}
File candidate = null;
for (File extracted : extractedFiles) {
if (!extracted.isDirectory() || !new File(extracted, "dimensions").isDirectory()) {
continue;
}
if (candidate != null) {
return work;
}
candidate = extracted;
}
if (candidate != null) {
return candidate;
}
return extractedFiles.length == 1 && extractedFiles[0].isDirectory() ? extractedFiles[0] : work;
}
static PackInstallResult installExtractedPack(File packsFolder, File extractedPack, boolean forceOverwrite, static PackInstallResult installExtractedPack(File packsFolder, File extractedPack, boolean forceOverwrite,
String expectedKey, Consumer<String> feedback) throws IOException { String expectedKey, Consumer<String> feedback) throws IOException {
Objects.requireNonNull(packsFolder, "packsFolder"); Objects.requireNonNull(packsFolder, "packsFolder");
@@ -311,7 +357,7 @@ public final class PackDownloader {
PackValidationResult stagedValidation; PackValidationResult stagedValidation;
try { try {
stagedValidation = PackValidator.validate(staging); stagedValidation = PackValidator.validateForDatapackBootstrap(staging);
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
throw new IOException("Pack validation failed before publication for '" + key + "'", exception); throw new IOException("Pack validation failed before publication for '" + key + "'", exception);
} }
@@ -375,8 +421,8 @@ public final class PackDownloader {
)); ));
return null; return null;
} }
boolean present = isManagedBetaPack(prepared.key()) boolean present = isManagedPack(prepared.key())
? isManagedBetaPackPresent(packsRoot.toFile(), prepared.key()) ? isManagedPackPresent(packsRoot.toFile(), prepared.key())
: isPackPresent(packsRoot.toFile(), prepared.key()); : isPackPresent(packsRoot.toFile(), prepared.key());
if (!forceOverwrite && present) { if (!forceOverwrite && present) {
sendFeedback(feedback, IrisLanguage.plain( sendFeedback(feedback, IrisLanguage.plain(
@@ -418,7 +464,7 @@ public final class PackDownloader {
PackDownloadMessages.ACQUIRED, PackDownloadMessages.ACQUIRED,
MessageArgument.untrusted("name", prepared.name()) MessageArgument.untrusted("name", prepared.name())
)); ));
return new PackInstallResult(prepared.key(), true, false); return new PackInstallResult(prepared.key(), true, true);
} }
private static Path findConflictingPack(Path packsRoot, Path staging, Path target, String key) throws IOException { private static Path findConflictingPack(Path packsRoot, Path staging, Path target, String key) throws IOException {
@@ -648,12 +694,20 @@ public final class PackDownloader {
return "https://codeload.github.com/" + repo + "/zip/" + qualifiedRef; return "https://codeload.github.com/" + repo + "/zip/" + qualifiedRef;
} }
static String defaultOverworldReleaseUrl() { static String defaultOverworldRepository() {
return DEFAULT_OVERWORLD_RELEASE_URL; return DEFAULT_OVERWORLD_REPOSITORY;
} }
static String underworldReleaseUrl() { static String defaultOverworldRef() {
return UNDERWORLD_RELEASE_URL; return DEFAULT_OVERWORLD_REF;
}
static String underworldRepository() {
return UNDERWORLD_REPOSITORY;
}
static String underworldRef() {
return UNDERWORLD_REF;
} }
private static void validateGithubRef(String qualifiedRef) { private static void validateGithubRef(String qualifiedRef) {
@@ -681,7 +735,7 @@ public final class PackDownloader {
private record PreparedPack(String key, String name, PackValidationResult validation) { private record PreparedPack(String key, String name, PackValidationResult validation) {
} }
private record ManagedBetaPack(String repository, String releaseUrl) { private record ManagedPack(String repository, String ref) {
} }
public record PackInstallResult(String key, boolean changed, boolean restartRequired) { public record PackInstallResult(String key, boolean changed, boolean restartRequired) {
@@ -98,14 +98,8 @@ public class StudioSVC implements IrisService {
@Override @Override
public void onEnable() { public void onEnable() {
VolmitSender console = BukkitPlatform.console();
runPackMutation(console, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD,
"managed-beta-packs", () -> installMissingManagedBetaPacks(console),
"Failed to install Iris managed beta packs at startup.");
String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType(); String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType();
if (!PackDownloader.isManagedBetaPack(configuredPack) if (!PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) {
&& !PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) {
IrisLogging.warn("Default pack '" + configuredPack IrisLogging.warn("Default pack '" + configuredPack
+ "' is not installed. Please download it manually with /iris download " + configuredPack); + "' is not installed. Please download it manually with /iris download " + configuredPack);
} }
@@ -342,25 +336,41 @@ public class StudioSVC implements IrisService {
public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) { public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) {
runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, key, () -> { runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, key, () -> {
DownloadOutcome outcome = downloadSearchLocked(sender, key, forceOverwrite); DownloadOutcome outcome = downloadSearchLocked(sender, key, forceOverwrite);
return finishStandalonePackMutation(sender, outcome); return finishStandalonePackMutation(outcome);
}, IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_DOWNLOAD, MessageArgument.untrusted("key", String.valueOf(key)))); }, IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_DOWNLOAD, MessageArgument.untrusted("key", String.valueOf(key))));
} }
public void downloadManagedBeta(VolmitSender sender, String key, boolean forceOverwrite) { public void downloadManaged(VolmitSender sender, String key, boolean forceOverwrite) {
if (!PackDownloader.isManagedBetaPack(key)) { if (!PackDownloader.isManagedPack(key)) {
sender.sendMessage("Iris pack '" + key + "' does not have a managed beta release."); sender.sendMessage("Iris pack '" + key + "' does not have a managed Git source.");
return; return;
} }
runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, key, () -> { runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, key, () -> {
DownloadOutcome outcome = downloadManagedBetaLocked(sender, key, forceOverwrite); DownloadOutcome outcome = downloadManagedLocked(sender, key, forceOverwrite);
return finishStandalonePackMutation(sender, outcome); return finishStandalonePackMutation(outcome);
}, "Failed to download IrisDimensions/" + key + " beta release."); }, "Failed to download managed Iris pack '" + key + "'.");
}
public void downloadUrl(VolmitSender sender, String url, boolean forceOverwrite) {
if (!PackDownloader.isDirectZipUrl(url)) {
sender.sendMessage("Iris requires a valid HTTP or HTTPS .zip URL.");
return;
}
runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, url, () -> {
DownloadOutcome outcome = DownloadOutcome.from(PackDownloader.downloadUrl(
getWorkspaceFolder(),
url,
forceOverwrite,
sender::sendMessage
));
return finishStandalonePackMutation(outcome);
}, "Failed to download Iris pack from '" + url + "'.");
} }
public void downloadBranch(VolmitSender sender, String repo, String branch, boolean forceOverwrite) { public void downloadBranch(VolmitSender sender, String repo, String branch, boolean forceOverwrite) {
runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, repo + "/" + branch, () -> { runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, repo + "/" + branch, () -> {
DownloadOutcome outcome = downloadLocked(sender, repo, branch, forceOverwrite, false, null); DownloadOutcome outcome = downloadLocked(sender, repo, branch, forceOverwrite, false, null);
return finishStandalonePackMutation(sender, outcome); return finishStandalonePackMutation(outcome);
}, IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_DOWNLOAD_BRANCH, MessageArgument.untrusted("repo", String.valueOf(repo)), MessageArgument.untrusted("branch", String.valueOf(branch)))); }, IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FAILED_DOWNLOAD_BRANCH, MessageArgument.untrusted("repo", String.valueOf(repo)), MessageArgument.untrusted("branch", String.valueOf(branch))));
} }
@@ -376,13 +386,13 @@ public class StudioSVC implements IrisService {
String target = expectedKey == null ? repo + "/" + branch : expectedKey; String target = expectedKey == null ? repo + "/" + branch : expectedKey;
runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, target, () -> { runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, target, () -> {
DownloadOutcome outcome = downloadLocked(sender, repo, branch, forceOverwrite, directUrl, expectedKey); DownloadOutcome outcome = downloadLocked(sender, repo, branch, forceOverwrite, directUrl, expectedKey);
return finishStandalonePackMutation(sender, outcome); return finishStandalonePackMutation(outcome);
}, "Failed to download Iris pack '" + target + "'."); }, "Failed to download Iris pack '" + target + "'.");
} }
private DownloadOutcome downloadSearchLocked(VolmitSender sender, String key, boolean forceOverwrite) throws IOException { private DownloadOutcome downloadSearchLocked(VolmitSender sender, String key, boolean forceOverwrite) throws IOException {
if (PackDownloader.isManagedBetaPack(key)) { if (PackDownloader.isManagedPack(key)) {
return downloadManagedBetaLocked(sender, key, forceOverwrite); return downloadManagedLocked(sender, key, forceOverwrite);
} }
String descriptor = key.contains("/") ? key : getListing(false).get(key); String descriptor = key.contains("/") ? key : getListing(false).get(key);
@@ -412,17 +422,17 @@ public class StudioSVC implements IrisService {
return new PackListingReference(repository, ref, expectedKey); return new PackListingReference(repository, ref, expectedKey);
} }
private DownloadOutcome downloadManagedBetaLocked( private DownloadOutcome downloadManagedLocked(
VolmitSender sender, VolmitSender sender,
String expectedKey, String expectedKey,
boolean forceOverwrite boolean forceOverwrite
) throws IOException { ) throws IOException {
if (!forceOverwrite && PackDownloader.isManagedBetaPackPresent(getWorkspaceFolder(), expectedKey)) { if (!forceOverwrite && PackDownloader.isManagedPackPresent(getWorkspaceFolder(), expectedKey)) {
sender.sendMessage(IrisLanguage.text(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey))); sender.sendMessage(IrisLanguage.text(PackDownloadMessages.ALREADY_INSTALLED, MessageArgument.untrusted("key", expectedKey)));
return DownloadOutcome.notChanged(); return DownloadOutcome.notChanged();
} }
PackDownloader.PackInstallResult result = PackDownloader.downloadManagedBeta( PackDownloader.PackInstallResult result = PackDownloader.downloadManaged(
getWorkspaceFolder(), getWorkspaceFolder(),
expectedKey, expectedKey,
forceOverwrite, forceOverwrite,
@@ -431,33 +441,6 @@ public class StudioSVC implements IrisService {
return DownloadOutcome.from(result); return DownloadOutcome.from(result);
} }
private boolean installMissingManagedBetaPacks(VolmitSender sender) {
boolean changed = false;
boolean restartRequired = false;
for (String key : missingManagedBetaPacks(getWorkspaceFolder())) {
IrisLogging.info("Downloading managed Iris pack " + key + " (beta release)");
try {
DownloadOutcome outcome = downloadManagedBetaLocked(sender, key, false);
changed |= outcome.changed();
restartRequired |= outcome.restartRequired();
} catch (Throwable failure) {
IrisLogging.reportError("Failed to download IrisDimensions/" + key + " beta release.", failure);
sender.sendMessage("Failed to download IrisDimensions/" + key + " beta release. " + errorDetail(failure));
}
}
return finishStandalonePackMutation(sender, new DownloadOutcome(changed, restartRequired));
}
static List<String> missingManagedBetaPacks(File workspaceFolder) {
List<String> missing = new ArrayList<>();
for (String key : PackDownloader.managedBetaPacks()) {
if (!PackDownloader.isManagedBetaPackPresent(workspaceFolder, key)) {
missing.add(key);
}
}
return List.copyOf(missing);
}
private DownloadOutcome downloadLocked( private DownloadOutcome downloadLocked(
VolmitSender sender, VolmitSender sender,
String repo, String repo,
@@ -1033,27 +1016,14 @@ public class StudioSVC implements IrisService {
} }
if (restartRequired) { if (restartRequired) {
sender.sendMessage("Iris must restart before the new pack data can be used."); sender.sendMessage("Restart the server before using the downloaded Iris pack.");
ServerConfigurator.restart();
} }
}; };
runOffPrimaryThread(work); runOffPrimaryThread(work);
} }
private boolean finishStandalonePackMutation(VolmitSender sender, DownloadOutcome outcome) { private boolean finishStandalonePackMutation(DownloadOutcome outcome) {
if (!outcome.changed()) { return outcome.changed() && outcome.restartRequired();
return false;
}
DatapackInstallResult installResult = ServerConfigurator.installDataPacksIfChanged(true);
return switch (installResult.status()) {
case FAILED -> {
sender.sendMessage("The pack was downloaded, but Iris could not install its datapack output.");
yield false;
}
case RESTART_REQUIRED -> true;
case READY, UNCHANGED -> outcome.restartRequired();
};
} }
private void runOffPrimaryThread(Runnable work) { private void runOffPrimaryThread(Runnable work) {
@@ -75,12 +75,7 @@ public class IrisToolbelt {
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod(); private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
/** /**
* Will find / download / search for the dimension or return null * Finds an installed dimension or returns null.
* <p>
* - You can provide a dimenson in the packs folder by the folder name
* - You can provide a github repo by using (assumes branch is master unless specified)
* - GithubUsername/repository
* - GithubUsername/repository/branch
* *
* @param dimension the dimension id such as overworld or flat * @param dimension the dimension id such as overworld or flat
* @return the IrisDimension or null * @return the IrisDimension or null
@@ -105,18 +100,9 @@ public class IrisToolbelt {
} }
if (pack == null) { if (pack == null) {
IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), BukkitPlatform.volmitPlugin().getTag()), reference.pack(), false); IrisLogging.warn("Iris pack '" + reference.pack()
String installedPackName = installedPackName(reference.pack()); + "' is not installed. Install it with /iris download " + reference.pack()
File found = PackDirectoryResolver.resolveExisting(packsFolder, installedPackName); + " and restart the server.");
if (found == null) {
found = findCaseInsensitivePack(packsFolder, installedPackName);
}
if (found != null) {
pack = found;
}
}
if (pack == null) {
return null; return null;
} }
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eDie Welt wurde anscheinend bereits aus bukkit.yml entfernt", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eDie Welt wurde anscheinend bereits aus bukkit.yml entfernt",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cbukkit.yml konnte wegen {value} nicht gespeichert werden", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cbukkit.yml konnte wegen {value} nicht gespeichert werden",
"iris.bukkit.commandiris.set_debug": "§aDebug gesetzt auf: {to}", "iris.bukkit.commandiris.set_debug": "§aDebug gesetzt auf: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aDownload des Pack: {pack} (Beta-Version){value}",
"iris.bukkit.commandiris.downloading_pack": "§aDownload des Pack: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aDownload des Pack: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cDu musst dich in einer Iris-Welt befinden", "iris.bukkit.commandiris.you_must_be_iris_world": "§cDu musst dich in einer Iris-Welt befinden",
"iris.bukkit.commandiris.sending_metrics": "§aÜbermittlung von Metriken...", "iris.bukkit.commandiris.sending_metrics": "§aÜbermittlung von Metriken...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pack '{key}' wurde nicht in der Pack-Liste gefunden.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pack '{key}' wurde nicht in der Pack-Liste gefunden.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Verwende /iris download <pack> branch=<branch>, um den Download manuell auszuführen.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Verwende /iris download <pack> branch=<branch>, um den Download manuell auszuführen.",
"iris.bukkit.runtime.studiosvc.failed_download": "Download von '{key}' fehlgeschlagen.", "iris.bukkit.runtime.studiosvc.failed_download": "Download von '{key}' fehlgeschlagen.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Download der Beta-Version IrisDimensions/overworld fehlgeschlagen.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Download von '{repo}' (Branch {branch}) fehlgeschlagen.", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Download von '{repo}' (Branch {branch}) fehlgeschlagen.",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Studio-Welt konnte nicht geöffnet werden: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Studio-Welt konnte nicht geöffnet werden: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Studio '{dimm}' kann nicht geöffnet werden das Pack enthält blockierende Fehler:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Studio '{dimm}' kann nicht geöffnet werden das Pack enthält blockierende Fehler:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eParece que el mundo ya se había eliminado de bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eParece que el mundo ya se había eliminado de bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cNo se pudo guardar bukkit.yml debido a {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cNo se pudo guardar bukkit.yml debido a {value}",
"iris.bukkit.commandiris.set_debug": "§aDepuración establecida en: {to}", "iris.bukkit.commandiris.set_debug": "§aDepuración establecida en: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aDescargando pack: {pack} (versión beta){value}",
"iris.bukkit.commandiris.downloading_pack": "§aDescargando pack: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aDescargando pack: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cDebes estar en un mundo de Iris", "iris.bukkit.commandiris.you_must_be_iris_world": "§cDebes estar en un mundo de Iris",
"iris.bukkit.commandiris.sending_metrics": "§aEnviando métricas...", "iris.bukkit.commandiris.sending_metrics": "§aEnviando métricas...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "El pack '{key}' no se encontró en la lista de packs.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "El pack '{key}' no se encontró en la lista de packs.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Usa /iris download <pack> branch=<branch> para descargarlo manualmente.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Usa /iris download <pack> branch=<branch> para descargarlo manualmente.",
"iris.bukkit.runtime.studiosvc.failed_download": "No se pudo descargar '{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "No se pudo descargar '{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "No se pudo descargar la versión beta de IrisDimensions/overworld.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "No se pudo descargar '{repo}' (rama {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "No se pudo descargar '{repo}' (rama {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "No se pudo abrir el mundo de Studio: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "No se pudo abrir el mundo de Studio: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "No se puede abrir Studio '{dimm}': el pack contiene errores bloqueantes:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "No se puede abrir Studio '{dimm}': el pack contiene errores bloqueantes:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eNäyttää siltä, että maailma on jo poistettu. bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eNäyttää siltä, että maailma on jo poistettu. bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cTallennus epäonnistui bukkit.yml koska {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cTallennus epäonnistui bukkit.yml koska {value}",
"iris.bukkit.commandiris.set_debug": "§aAseta vianetsintä: {to}", "iris.bukkit.commandiris.set_debug": "§aAseta vianetsintä: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aNoudetaan pakkaus: {pack} (beetan vapautuminen){value}",
"iris.bukkit.commandiris.downloading_pack": "§aNoudetaan pakkaus: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aNoudetaan pakkaus: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cSinun täytyy olla Iris maailma", "iris.bukkit.commandiris.you_must_be_iris_world": "§cSinun täytyy olla Iris maailma",
"iris.bukkit.commandiris.sending_metrics": "§aLähetetään mittareita...", "iris.bukkit.commandiris.sending_metrics": "§aLähetetään mittareita...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pakkaus{key}' ei löytynyt pakkauslistalta.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pakkaus{key}' ei löytynyt pakkauslistalta.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Käyttö /iris download <pack> branch=<branch> ladata käsin.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Käyttö /iris download <pack> branch=<branch> ladata käsin.",
"iris.bukkit.runtime.studiosvc.failed_download": "Ei voitu ladata \"{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Ei voitu ladata \"{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Ei voitu ladata IrisDimensionsOverworld beta -julkaisu.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Ei voitu ladata \"{repo}\" (ala {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Ei voitu ladata \"{repo}\" (ala {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Studiomaailman avaaminen epäonnistui: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Studiomaailman avaaminen epäonnistui: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Studiota ei voi avata{dimm}\" - pakkauksessa on estovirheitä:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Studiota ei voi avata{dimm}\" - pakkauksessa on estovirheitä:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eIl semble que le monde avait déjà été supprimé de bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eIl semble que le monde avait déjà été supprimé de bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cImpossible d'enregistrer bukkit.yml à cause de {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cImpossible d'enregistrer bukkit.yml à cause de {value}",
"iris.bukkit.commandiris.set_debug": "§aDébogage défini sur : {to}", "iris.bukkit.commandiris.set_debug": "§aDébogage défini sur : {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aTéléchargement du pack : {pack} (version bêta){value}",
"iris.bukkit.commandiris.downloading_pack": "§aTéléchargement du pack : {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aTéléchargement du pack : {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cVous devez être dans un monde Iris", "iris.bukkit.commandiris.you_must_be_iris_world": "§cVous devez être dans un monde Iris",
"iris.bukkit.commandiris.sending_metrics": "§aEnvoi des métriques...", "iris.bukkit.commandiris.sending_metrics": "§aEnvoi des métriques...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Le pack '{key}' est introuvable dans la liste des packs.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Le pack '{key}' est introuvable dans la liste des packs.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Utilisez /iris download <pack> branch=<branch> pour le télécharger manuellement.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Utilisez /iris download <pack> branch=<branch> pour le télécharger manuellement.",
"iris.bukkit.runtime.studiosvc.failed_download": "Impossible de télécharger '{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Impossible de télécharger '{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Impossible de télécharger la version bêta de IrisDimensions/overworld.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Impossible de télécharger '{repo}' (branche {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Impossible de télécharger '{repo}' (branche {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Impossible d'ouvrir le monde Studio : {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Impossible d'ouvrir le monde Studio : {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Impossible d'ouvrir Studio '{dimm}' : le pack contient des erreurs bloquantes :", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Impossible d'ouvrir Studio '{dimm}' : le pack contient des erreurs bloquantes :",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eנראה כאילו העולם כבר הוסר bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eנראה כאילו העולם כבר הוסר bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cנכשל להציל bukkit.yml בגלל {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cנכשל להציל bukkit.yml בגלל {value}",
"iris.bukkit.commandiris.set_debug": "§aהגדר bug: {to}", "iris.bukkit.commandiris.set_debug": "§aהגדר bug: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aהורדת חבילות: {pack} (שחרור בטא){value}",
"iris.bukkit.commandiris.downloading_pack": "§aהורדת חבילות: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aהורדת חבילות: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cאתה חייב להיות בתוך Iris עולם העולם", "iris.bukkit.commandiris.you_must_be_iris_world": "§cאתה חייב להיות בתוך Iris עולם העולם",
"iris.bukkit.commandiris.sending_metrics": "§aשליחת מדדים...", "iris.bukkit.commandiris.sending_metrics": "§aשליחת מדדים...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pack »{key}\"לא נמצאו בחבילה.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pack »{key}\"לא נמצאו בחבילה.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "שימוש בשימוש /iris download <pack> branch=<branch> להוריד באופן ידני.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "שימוש בשימוש /iris download <pack> branch=<branch> להוריד באופן ידני.",
"iris.bukkit.runtime.studiosvc.failed_download": "נכשל להוריד את \"{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "נכשל להוריד את \"{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "הורדת גרסת הבטא של IrisDimensions/overworld נכשלה.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "נכשל להוריד את \"{repo}(ברנץ' {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "נכשל להוריד את \"{repo}(ברנץ' {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "נכשל לפתוח את עולם הסטודיו: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "נכשל לפתוח את עולם הסטודיו: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "\"לא יכול לפתוח סטודיו\"{dimm}חבילה חוסמת שגיאות:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "\"לא יכול לפתוח סטודיו\"{dimm}חבילה חוסמת שגיאות:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eSembra che il mondo sia già stato rimosso da bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eSembra che il mondo sia già stato rimosso da bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cImpossibile salvare bukkit.yml a causa di {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cImpossibile salvare bukkit.yml a causa di {value}",
"iris.bukkit.commandiris.set_debug": "§aDebug impostato su: {to}", "iris.bukkit.commandiris.set_debug": "§aDebug impostato su: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aDownload del pack: {pack} (versione beta){value}",
"iris.bukkit.commandiris.downloading_pack": "§aDownload del pack: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aDownload del pack: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cDevi trovarti in un mondo Iris", "iris.bukkit.commandiris.you_must_be_iris_world": "§cDevi trovarti in un mondo Iris",
"iris.bukkit.commandiris.sending_metrics": "§aInvio di metriche...", "iris.bukkit.commandiris.sending_metrics": "§aInvio di metriche...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Il Pack '{key}' non è stato trovato nell'elenco dei Pack.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Il Pack '{key}' non è stato trovato nell'elenco dei Pack.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Usa /iris download <pack> branch=<branch> per eseguire manualmente il download.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Usa /iris download <pack> branch=<branch> per eseguire manualmente il download.",
"iris.bukkit.runtime.studiosvc.failed_download": "Download di '{key}' non riuscito.", "iris.bukkit.runtime.studiosvc.failed_download": "Download di '{key}' non riuscito.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Download della versione beta IrisDimensions/overworld non riuscito.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Download di '{repo}' (branch {branch}) non riuscito.", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Download di '{repo}' (branch {branch}) non riuscito.",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Impossibile aprire il mondo studio: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Impossibile aprire il mondo studio: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Impossibile aprire lo studio '{dimm}': il Pack contiene errori bloccanti:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Impossibile aprire lo studio '{dimm}': il Pack contiene errori bloccanti:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eワールドはすでに bukkit.yml から削除されているようです", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eワールドはすでに bukkit.yml から削除されているようです",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cbukkit.yml を保存できませんでした: {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cbukkit.yml を保存できませんでした: {value}",
"iris.bukkit.commandiris.set_debug": "§aデバッグ設定: {to}", "iris.bukkit.commandiris.set_debug": "§aデバッグ設定: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aパックをダウンロードしています: {pack}(ベータリリース){value}",
"iris.bukkit.commandiris.downloading_pack": "§aパックをダウンロードしています: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aパックをダウンロードしています: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cIris ワールド内にいる必要があります", "iris.bukkit.commandiris.you_must_be_iris_world": "§cIris ワールド内にいる必要があります",
"iris.bukkit.commandiris.sending_metrics": "§aメトリックの送信...", "iris.bukkit.commandiris.sending_metrics": "§aメトリックの送信...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "パック一覧にパック '{key}' が見つかりません。", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "パック一覧にパック '{key}' が見つかりません。",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "手動でダウンロードするには /iris download <pack> branch=<branch> を使用してください。", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "手動でダウンロードするには /iris download <pack> branch=<branch> を使用してください。",
"iris.bukkit.runtime.studiosvc.failed_download": "'{key}' のダウンロードに失敗しました。", "iris.bukkit.runtime.studiosvc.failed_download": "'{key}' のダウンロードに失敗しました。",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "IrisDimensions/overworld のベータリリースをダウンロードできませんでした。",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "'{repo}'(ブランチ {branch})をダウンロードできませんでした。", "iris.bukkit.runtime.studiosvc.failed_download_branch": "'{repo}'(ブランチ {branch})をダウンロードできませんでした。",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "スタジオワールドを開けませんでした: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "スタジオワールドを開けませんでした: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "スタジオ '{dimm}' を開けません。パックに処理を妨げるエラーがあります:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "スタジオ '{dimm}' を開けません。パックに処理を妨げるエラーがあります:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§e세상과 같은 모습은 이미 제거되었습니다. bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§e세상과 같은 모습은 이미 제거되었습니다. bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§c{value} 때문에 bukkit.yml을 저장하지 못했습니다", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§c{value} 때문에 bukkit.yml을 저장하지 못했습니다",
"iris.bukkit.commandiris.set_debug": "§a디버그 설정: {to}", "iris.bukkit.commandiris.set_debug": "§a디버그 설정: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§a다운로드 팩: {pack} (베타 릴리스){value}",
"iris.bukkit.commandiris.downloading_pack": "§a다운로드 팩: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§a다운로드 팩: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cIris 월드 안에 있어야 합니다", "iris.bukkit.commandiris.you_must_be_iris_world": "§cIris 월드 안에 있어야 합니다",
"iris.bukkit.commandiris.sending_metrics": "§a메트릭 전송...", "iris.bukkit.commandiris.sending_metrics": "§a메트릭 전송...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "팩 '{key}'는 팩 목록에서 찾을 수 없습니다.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "팩 '{key}'는 팩 목록에서 찾을 수 없습니다.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "수동으로 다운로드하려면 /iris download <pack> branch=<branch>을(를) 사용하세요.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "수동으로 다운로드하려면 /iris download <pack> branch=<branch>을(를) 사용하세요.",
"iris.bukkit.runtime.studiosvc.failed_download": "다운로드 실패 '{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "다운로드 실패 '{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "다운로드 실패 IrisDimensions/overworld 베타 릴리스.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "다운로드 실패 '{repo}' (branch {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "다운로드 실패 '{repo}' (branch {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "열린 스튜디오 세계에 실패: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "열린 스튜디오 세계에 실패: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "열린 스튜디오 '{dimm}' - 팩은 오류를 차단했습니다.", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "열린 스튜디오 '{dimm}' - 팩은 오류를 차단했습니다.",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eAtrodo, pasaulis jau buvo pašalintas iš bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eAtrodo, pasaulis jau buvo pašalintas iš bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cNepavyko įrašyti bukkit.yml dėl {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cNepavyko įrašyti bukkit.yml dėl {value}",
"iris.bukkit.commandiris.set_debug": "§aNustatyti derinimą į: {to}", "iris.bukkit.commandiris.set_debug": "§aNustatyti derinimą į: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aSiunčiama pakuotė: {pack} (beta atsipalaidavimas){value}",
"iris.bukkit.commandiris.downloading_pack": "§aSiunčiama pakuotė: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aSiunčiama pakuotė: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cJūs turite būti Iris pasaulis", "iris.bukkit.commandiris.you_must_be_iris_world": "§cJūs turite būti Iris pasaulis",
"iris.bukkit.commandiris.sending_metrics": "§aSiunčiami metrikos...", "iris.bukkit.commandiris.sending_metrics": "§aSiunčiami metrikos...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pakuotė \"{key}\"nebuvo rasta pakuotės sąraše.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pakuotė \"{key}\"nebuvo rasta pakuotės sąraše.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Naudojimas /iris download <pack> branch=<branch> atsisiųsti rankiniu būdu.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Naudojimas /iris download <pack> branch=<branch> atsisiųsti rankiniu būdu.",
"iris.bukkit.runtime.studiosvc.failed_download": "Nepavyko atsisiųsti \"{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Nepavyko atsisiųsti \"{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Nepavyko atsisiųsti IrisDimensions/ overworld beta release.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Nepavyko atsisiųsti \"{repo}'(šaka {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Nepavyko atsisiųsti \"{repo}'(šaka {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Nepavyko atverti studijos pasaulio: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Nepavyko atverti studijos pasaulio: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Nepavyko atverti studijos \"{dimm}\"- pakuotė turi blokavimo klaidų:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Nepavyko atverti studijos \"{dimm}\"- pakuotė turi blokavimo klaidų:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eHet lijkt erop dat de wereld al verwijderd was van bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eHet lijkt erop dat de wereld al verwijderd was van bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cOpslaan is mislukt bukkit.yml vanwege {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cOpslaan is mislukt bukkit.yml vanwege {value}",
"iris.bukkit.commandiris.set_debug": "§aDebug instellen op: {to}", "iris.bukkit.commandiris.set_debug": "§aDebug instellen op: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aDownloadpakket: {pack} (beta vrijgeven){value}",
"iris.bukkit.commandiris.downloading_pack": "§aDownloadpakket: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aDownloadpakket: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cJe moet in een Iris wereld", "iris.bukkit.commandiris.you_must_be_iris_world": "§cJe moet in een Iris wereld",
"iris.bukkit.commandiris.sending_metrics": "§aVerzenden van statistieken...", "iris.bukkit.commandiris.sending_metrics": "§aVerzenden van statistieken...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Verpakking{key}' werd niet gevonden in de verpakking lijst.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Verpakking{key}' werd niet gevonden in de verpakking lijst.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Gebruik /iris download <pack> branch=<branch> handmatig downloaden.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Gebruik /iris download <pack> branch=<branch> handmatig downloaden.",
"iris.bukkit.runtime.studiosvc.failed_download": "Downloaden mislukt{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Downloaden mislukt{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Downloaden van de IrisDimensions/overworld bèta release.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Downloaden mislukt{repo}\" (tak {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Downloaden mislukt{repo}\" (tak {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Openen van studiowereld mislukt: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Openen van studiowereld mislukt: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Kan studio niet openen '{dimm}' - Pack heeft blokkerende fouten:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Kan studio niet openen '{dimm}' - Pack heeft blokkerende fouten:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eWygląda na to, że świat został już usunięty z bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eWygląda na to, że świat został już usunięty z bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cNie udało się zapisać bukkit.yml z powodu {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cNie udało się zapisać bukkit.yml z powodu {value}",
"iris.bukkit.commandiris.set_debug": "§aUstaw debugowanie na: {to}", "iris.bukkit.commandiris.set_debug": "§aUstaw debugowanie na: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aPobieranie opakowania: {pack} (uwalnianie beta){value}",
"iris.bukkit.commandiris.downloading_pack": "§aPobieranie opakowania: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aPobieranie opakowania: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cMusisz być w Iris świat", "iris.bukkit.commandiris.you_must_be_iris_world": "§cMusisz być w Iris świat",
"iris.bukkit.commandiris.sending_metrics": "§aWysyłanie wskaźników...", "iris.bukkit.commandiris.sending_metrics": "§aWysyłanie wskaźników...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Paczka \"{key}'nie znaleziono go na liście.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Paczka \"{key}'nie znaleziono go na liście.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Stosowanie /iris download <pack> branch=<branch> aby pobrać ręcznie.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Stosowanie /iris download <pack> branch=<branch> aby pobrać ręcznie.",
"iris.bukkit.runtime.studiosvc.failed_download": "Nie można pobrać '{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Nie można pobrać '{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Nie udało się pobrać IrisDimensions/ Overworld beta wydania.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Nie można pobrać '{repo}'(oddział {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Nie można pobrać '{repo}'(oddział {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Nie udało się otworzyć studio świata: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Nie udało się otworzyć studio świata: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Nie można otworzyć studia \"{dimm}\"- opakowanie ma błędy blokujące:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Nie można otworzyć studia \"{dimm}\"- opakowanie ma błędy blokujące:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eParece que o mundo já foi removido de bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eParece que o mundo já foi removido de bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cFalha ao salvar bukkit.yml devido a {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cFalha ao salvar bukkit.yml devido a {value}",
"iris.bukkit.commandiris.set_debug": "§aDefinir depuração como: {to}", "iris.bukkit.commandiris.set_debug": "§aDefinir depuração como: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aBaixando pacote: {pack} (versão beta){value}",
"iris.bukkit.commandiris.downloading_pack": "§aBaixando pacote: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aBaixando pacote: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cVocê deve estar em um Iris mundo", "iris.bukkit.commandiris.you_must_be_iris_world": "§cVocê deve estar em um Iris mundo",
"iris.bukkit.commandiris.sending_metrics": "§aEnviando métricas...", "iris.bukkit.commandiris.sending_metrics": "§aEnviando métricas...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Embalar '{key}' não foi encontrado na lista de pacotes.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Embalar '{key}' não foi encontrado na lista de pacotes.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Utilização /iris download <pack> branch=<branch> para baixar manualmente.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Utilização /iris download <pack> branch=<branch> para baixar manualmente.",
"iris.bukkit.runtime.studiosvc.failed_download": "Falha ao transferir '{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Falha ao transferir '{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Não foi possível transferir o IrisDimensions- Liberação beta.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Falha ao transferir '{repo}' (ramo {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Falha ao transferir '{repo}' (ramo {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Falha ao abrir o mundo do estúdio: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Falha ao abrir o mundo do estúdio: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Não foi possível abrir o estúdio '{dimm}- a pack tem erros de bloqueio:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Não foi possível abrir o estúdio '{dimm}- a pack tem erros de bloqueio:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eПохоже, мир уже был удален из bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eПохоже, мир уже был удален из bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cНе удалось спасти bukkit.yml Потому что {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cНе удалось спасти bukkit.yml Потому что {value}",
"iris.bukkit.commandiris.set_debug": "§aУстановить отладку для: {to}", "iris.bukkit.commandiris.set_debug": "§aУстановить отладку для: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aСкачать пакет: {pack} (бета-релиз){value}",
"iris.bukkit.commandiris.downloading_pack": "§aСкачать пакет: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aСкачать пакет: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cВы должны быть в Iris мир", "iris.bukkit.commandiris.you_must_be_iris_world": "§cВы должны быть в Iris мир",
"iris.bukkit.commandiris.sending_metrics": "§aОтправка метрик...", "iris.bukkit.commandiris.sending_metrics": "§aОтправка метрик...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Пакуй.{key}Не было найдено в списке упаковки.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Пакуй.{key}Не было найдено в списке упаковки.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Использовать /iris download <pack> branch=<branch> Скачать вручную.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Использовать /iris download <pack> branch=<branch> Скачать вручную.",
"iris.bukkit.runtime.studiosvc.failed_download": "Не удалось скачать{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Не удалось скачать{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Не удалось скачать IrisDimensions/overworld бета-версия.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Не удалось скачать{repo}' (ветка {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Не удалось скачать{repo}' (ветка {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Не удалось открыть студию: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Не удалось открыть студию: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Не может открыть студию{dimm}У пакета есть ошибки блокировки:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Не может открыть студию{dimm}У пакета есть ошибки блокировки:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eDünya gibi görünüyor zaten kaldırıldı bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eDünya gibi görünüyor zaten kaldırıldı bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§ckurtarmak için başarısız oldu bukkit.yml Çünkü {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§ckurtarmak için başarısız oldu bukkit.yml Çünkü {value}",
"iris.bukkit.commandiris.set_debug": "§aSet debug: {to}", "iris.bukkit.commandiris.set_debug": "§aSet debug: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aİndirme paketi: {pack} (Bea release){value}",
"iris.bukkit.commandiris.downloading_pack": "§aİndirme paketi: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aİndirme paketi: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cBirde olmalısın Iris dünya dünyası", "iris.bukkit.commandiris.you_must_be_iris_world": "§cBirde olmalısın Iris dünya dünyası",
"iris.bukkit.commandiris.sending_metrics": "§ametrikleri gönderin...", "iris.bukkit.commandiris.sending_metrics": "§ametrikleri gönderin...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pack \"{key}\" paketin listesinde bulunamadı.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Pack \"{key}\" paketin listesinde bulunamadı.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Kullanım Kullanımı /iris download <pack> branch=<branch> manuel olarak indirmek için.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Kullanım Kullanımı /iris download <pack> branch=<branch> manuel olarak indirmek için.",
"iris.bukkit.runtime.studiosvc.failed_download": "indirmek için başarısız oldu '{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "indirmek için başarısız oldu '{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "indirmek için başarısız oldu IrisDimensions/overworld beta serbest bırakılması.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "indirmek için başarısız oldu '{repo}\" (branch {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "indirmek için başarısız oldu '{repo}\" (branch {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Stüdyo dünyasını açmaya başarısız oldu: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Stüdyo dünyasını açmaya başarısız oldu: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Açık stüdyo \"{dimm}\" - paket hataları engelliyor:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Açık stüdyo \"{dimm}\" - paket hataları engelliyor:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eCó vẻ như thế giới đã bị loại bỏ bukkit.yml", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§eCó vẻ như thế giới đã bị loại bỏ bukkit.yml",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cLỗi lưu bukkit.yml bởi vì {value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§cLỗi lưu bukkit.yml bởi vì {value}",
"iris.bukkit.commandiris.set_debug": "§aĐặt lỗi là: {to}", "iris.bukkit.commandiris.set_debug": "§aĐặt lỗi là: {to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§aĐang tải về: {pack} (beta phát hành){value}",
"iris.bukkit.commandiris.downloading_pack": "§aĐang tải về: {pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§aĐang tải về: {pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§cChắc anh đang ở trong... Iris thế giới", "iris.bukkit.commandiris.you_must_be_iris_world": "§cChắc anh đang ở trong... Iris thế giới",
"iris.bukkit.commandiris.sending_metrics": "§aName...", "iris.bukkit.commandiris.sending_metrics": "§aName...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Gói '{key}Không tìm thấy trong danh sách gói hàng.", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "Gói '{key}Không tìm thấy trong danh sách gói hàng.",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Dùng /iris download <pack> branch=<branch> tải về bằng tay.", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "Dùng /iris download <pack> branch=<branch> tải về bằng tay.",
"iris.bukkit.runtime.studiosvc.failed_download": "Lỗi tải xuống{key}'.", "iris.bukkit.runtime.studiosvc.failed_download": "Lỗi tải xuống{key}'.",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "Việc tải về bị lỗi IrisDimensionsSự giải phóng của thế giới Bta.",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "Lỗi tải xuống{repo}' (branch {branch}).", "iris.bukkit.runtime.studiosvc.failed_download_branch": "Lỗi tải xuống{repo}' (branch {branch}).",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Lỗi mở thế giới studio: {error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "Lỗi mở thế giới studio: {error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Không thể mở studio{dimm}- gói có lỗi chặn:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "Không thể mở studio{dimm}- gói có lỗi chặn:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§e该世界似乎已从 bukkit.yml 中移除。", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§e该世界似乎已从 bukkit.yml 中移除。",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§c无法保存 bukkit.yml{value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§c无法保存 bukkit.yml{value}",
"iris.bukkit.commandiris.set_debug": "§a调试模式已设为:{to}", "iris.bukkit.commandiris.set_debug": "§a调试模式已设为:{to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§a正在下载包:{pack}Beta 版本){value}",
"iris.bukkit.commandiris.downloading_pack": "§a正在下载包:{pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§a正在下载包:{pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§c你必须位于 Iris 世界中。", "iris.bukkit.commandiris.you_must_be_iris_world": "§c你必须位于 Iris 世界中。",
"iris.bukkit.commandiris.sending_metrics": "§a正在发送指标...", "iris.bukkit.commandiris.sending_metrics": "§a正在发送指标...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "内容包“{key}”不在内容包列表中。", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "内容包“{key}”不在内容包列表中。",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "使用 /iris download <pack> branch=<branch> 手动下载。", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "使用 /iris download <pack> branch=<branch> 手动下载。",
"iris.bukkit.runtime.studiosvc.failed_download": "无法下载“{key}”。", "iris.bukkit.runtime.studiosvc.failed_download": "无法下载“{key}”。",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "无法下载 IrisDimensions/overworld 测试版。",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "无法下载“{repo}”(分支 {branch})。", "iris.bukkit.runtime.studiosvc.failed_download_branch": "无法下载“{repo}”(分支 {branch})。",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "无法打开工作室世界:{error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "无法打开工作室世界:{error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "无法打开工作室“{dimm}”:内容包存在阻断错误:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "无法打开工作室“{dimm}”:内容包存在阻断错误:",
@@ -179,7 +179,6 @@
"iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§e該世界似乎已從 bukkit.yml 中移除。", "iris.bukkit.commandiris.looks_like_world_was_already_removed_from_bukkit_yml": "§e該世界似乎已從 bukkit.yml 中移除。",
"iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§c無法儲存 bukkit.yml{value}", "iris.bukkit.commandiris.failed_save_bukkit_yml_because": "§c無法儲存 bukkit.yml{value}",
"iris.bukkit.commandiris.set_debug": "§a偵錯模式已設為:{to}", "iris.bukkit.commandiris.set_debug": "§a偵錯模式已設為:{to}",
"iris.bukkit.commandiris.downloading_pack_beta_release": "§a正在下載包:{pack}Beta 版本){value}",
"iris.bukkit.commandiris.downloading_pack": "§a正在下載包:{pack}/{branch}{value}", "iris.bukkit.commandiris.downloading_pack": "§a正在下載包:{pack}/{branch}{value}",
"iris.bukkit.commandiris.you_must_be_iris_world": "§c你必須位於 Iris 世界中。", "iris.bukkit.commandiris.you_must_be_iris_world": "§c你必須位於 Iris 世界中。",
"iris.bukkit.commandiris.sending_metrics": "§a正在傳送指標...", "iris.bukkit.commandiris.sending_metrics": "§a正在傳送指標...",
@@ -1153,7 +1152,6 @@
"iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "內容包「{key}」不在內容包清單中。", "iris.bukkit.runtime.studiosvc.pack_was_not_found_pack_listing": "內容包「{key}」不在內容包清單中。",
"iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "使用 /iris download <pack> branch=<branch> 手動下載。", "iris.bukkit.runtime.studiosvc.use_iris_download_pack_branch_branch_download_manually": "使用 /iris download <pack> branch=<branch> 手動下載。",
"iris.bukkit.runtime.studiosvc.failed_download": "無法下載「{key}」。", "iris.bukkit.runtime.studiosvc.failed_download": "無法下載「{key}」。",
"iris.bukkit.runtime.studiosvc.failed_download_irisdimensions_overworld_beta_release": "無法下載 IrisDimensions/overworld 測試版。",
"iris.bukkit.runtime.studiosvc.failed_download_branch": "無法下載「{repo}」(分支 {branch})。", "iris.bukkit.runtime.studiosvc.failed_download_branch": "無法下載「{repo}」(分支 {branch})。",
"iris.bukkit.runtime.studiosvc.failed_open_studio_world": "無法開啟工作室世界:{error}", "iris.bukkit.runtime.studiosvc.failed_open_studio_world": "無法開啟工作室世界:{error}",
"iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "無法開啟工作室「{dimm}」:內容包存在阻斷錯誤:", "iris.bukkit.runtime.studiosvc.cannot_open_studio_pack_has_blocking_errors": "無法開啟工作室「{dimm}」:內容包存在阻斷錯誤:",
@@ -37,23 +37,45 @@ import static org.junit.Assert.assertTrue;
public class DefaultPackBootstrapProvisionerTest { public class DefaultPackBootstrapProvisionerTest {
@Test @Test
public void defaultBetaSourcesArePinnedPerRequiredPack() { public void startupDoesNotRequireOrDownloadDefaultPacks() {
Map<String, DefaultPackBootstrapProvisioner.PackSpec> packs = new LinkedHashMap<>(); assertTrue(DefaultPackBootstrapProvisioner.defaultPacks().isEmpty());
for (DefaultPackBootstrapProvisioner.PackSpec pack : DefaultPackBootstrapProvisioner.defaultPacks()) { }
packs.put(pack.key(), pack);
}
assertEquals(2, packs.size()); @Test
assertEquals( public void emptyInstallPublishesValidDatapackWithoutNetworkRequests() throws Exception {
URI.create("https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip"), AtomicInteger requests = new AtomicInteger();
packs.get("overworld").source() HttpServer server = server(packArchive("overworld", "unused"), requests);
); Path root = Files.createTempDirectory("iris-bootstrap-empty");
assertEquals("overworld", packs.get("overworld").requiredDimension()); try {
assertEquals( Path dataDirectory = root.resolve("plugins/Iris");
URI.create("https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip"), DefaultPackBootstrapProvisioner.ProvisionOptions options = new DefaultPackBootstrapProvisioner.ProvisionOptions(
packs.get("underworld").source() List.of(),
); HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(),
assertEquals("underworld", packs.get("underworld").requiredDimension()); Clock.fixed(Instant.parse("2026-07-12T12:00:00Z"), ZoneOffset.UTC),
Duration.ofHours(1),
Duration.ofSeconds(2),
1,
Duration.ZERO,
8L * 1024L * 1024L,
root
);
DefaultPackBootstrapProvisioner.ProvisionResult result = DefaultPackBootstrapProvisioner.provision(
dataDirectory,
ignored -> {
},
options
);
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, result.status());
assertTrue(result.packRoots().isEmpty());
assertEquals(0, requests.get());
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("pack.mcmeta")));
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(dataDirectory, root, List.of()));
} finally {
server.stop(0);
delete(root);
}
} }
@Test @Test
@@ -31,8 +31,10 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.mockito.Answers; import org.mockito.Answers;
import com.sun.net.httpserver.HttpServer;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -44,6 +46,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -53,6 +56,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -71,6 +75,13 @@ public class PackDownloaderTest {
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS); IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
PlatformStructureHooks structureHooks = mock(PlatformStructureHooks.class, Answers.CALLS_REAL_METHODS); PlatformStructureHooks structureHooks = mock(PlatformStructureHooks.class, Answers.CALLS_REAL_METHODS);
when(platform.dataFolder()).thenReturn(temp.getRoot()); when(platform.dataFolder()).thenReturn(temp.getRoot());
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
File file = temp.getRoot();
for (Object argument : invocation.getArguments()) {
file = new File(file, String.valueOf(argument));
}
return file;
});
when(platform.structureHooks()).thenReturn(structureHooks); when(platform.structureHooks()).thenReturn(structureHooks);
when(structureHooks.structureKeys()).thenReturn(List.of("minecraft:village")); when(structureHooks.structureKeys()).thenReturn(List.of("minecraft:village"));
when(structureHooks.jigsawStructureKeys()).thenReturn(List.of("minecraft:village")); when(structureHooks.jigsawStructureKeys()).thenReturn(List.of("minecraft:village"));
@@ -90,23 +101,74 @@ public class PackDownloaderTest {
} }
@Test @Test
public void resolvesDefaultOverworldBetaRelease() { public void resolvesDefaultOverworldGitSource() {
assertEquals( assertEquals("IrisDimensions/overworld", PackDownloader.defaultOverworldRepository());
"https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip", assertEquals("master", PackDownloader.defaultOverworldRef());
PackDownloader.defaultOverworldReleaseUrl()
);
assertTrue(PackDownloader.isDefaultOverworld("overworld")); assertTrue(PackDownloader.isDefaultOverworld("overworld"));
assertTrue(PackDownloader.isManagedBetaPack("overworld")); assertTrue(PackDownloader.isManagedPack("overworld"));
assertEquals(List.of("overworld", "underworld"), PackDownloader.managedBetaPacks()); assertEquals(List.of("overworld", "underworld"), PackDownloader.managedPacks());
} }
@Test @Test
public void resolvesUnderworldBetaRelease() { public void resolvesUnderworldGitSource() {
assertEquals( assertEquals("IrisDimensions/underworld", PackDownloader.underworldRepository());
"https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip", assertEquals("main", PackDownloader.underworldRef());
PackDownloader.underworldReleaseUrl() assertTrue(PackDownloader.isManagedPack("underworld"));
); }
assertTrue(PackDownloader.isManagedBetaPack("underworld"));
@Test
public void recognizesOnlyHttpZipUrlsAsDirectSources() {
assertTrue(PackDownloader.isDirectZipUrl("https://packs.example.test/overworld.zip"));
assertTrue(PackDownloader.isDirectZipUrl("http://127.0.0.1/pack.ZIP?token=value"));
assertFalse(PackDownloader.isDirectZipUrl("https://packs.example.test/overworld.tar.gz"));
assertFalse(PackDownloader.isDirectZipUrl("file:///tmp/overworld.zip"));
assertFalse(PackDownloader.isDirectZipUrl("not-a-url"));
}
@Test
public void directZipUrlInstallsSingleDimensionPackAndRequiresRestart() throws Exception {
Path archive = temp.newFile("direct-pack.zip").toPath();
Map<String, String> entries = new LinkedHashMap<>();
entries.put("README.md", "Direct pack");
entries.put("wrapped/dimensions/direct_pack.json", "{\"name\":\"Direct\",\"regions\":[\"local\"],"
+ "\"structures\":[{\"nativeStructures\":[{\"structure\":\"test:future_structure\"}]}],"
+ "\"logicalHeight\":256,\"dimensionHeight\":{\"min\":-64,\"max\":320}}");
entries.put("wrapped/regions/local.json", "{\"name\":\"Local\",\"landBiomes\":[\"local\"]}");
entries.put("wrapped/biomes/local.json", "{\"name\":\"Local\",\"derivative\":\"minecraft:plains\"}");
writeArchive(archive, entries);
byte[] response = Files.readAllBytes(archive);
AtomicInteger requests = new AtomicInteger();
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/direct-pack.zip", exchange -> {
requests.incrementAndGet();
exchange.sendResponseHeaders(200, response.length);
exchange.getResponseBody().write(response);
exchange.close();
});
server.start();
try {
File packsFolder = temp.newFolder("direct-url-packs");
String url = "http://127.0.0.1:" + server.getAddress().getPort() + "/direct-pack.zip";
PackDownloader.PackInstallResult result = PackDownloader.downloadUrl(
packsFolder,
url,
false,
ignored -> {
}
);
assertNotNull(result);
assertEquals("direct_pack", result.key());
assertTrue(result.changed());
assertTrue(result.restartRequired());
assertEquals(1, requests.get());
assertTrue(Files.isRegularFile(
packsFolder.toPath().resolve("direct_pack/dimensions/direct_pack.json")
));
} finally {
server.stop(0);
}
} }
@Test @Test
@@ -118,11 +180,11 @@ public class PackDownloaderTest {
writeDimension(packsFolder.toPath().resolve("underworld"), "underworld_roof"); writeDimension(packsFolder.toPath().resolve("underworld"), "underworld_roof");
assertTrue(PackDownloader.isPackPresent(packsFolder, "underworld")); assertTrue(PackDownloader.isPackPresent(packsFolder, "underworld"));
assertFalse(PackDownloader.isManagedBetaPackPresent(packsFolder, "underworld")); assertFalse(PackDownloader.isManagedPackPresent(packsFolder, "underworld"));
writeDimension(packsFolder.toPath().resolve("underworld"), "underworld"); writeDimension(packsFolder.toPath().resolve("underworld"), "underworld");
assertTrue(Files.isDirectory(dimensions)); assertTrue(Files.isDirectory(dimensions));
assertTrue(PackDownloader.isManagedBetaPackPresent(packsFolder, "underworld")); assertTrue(PackDownloader.isManagedPackPresent(packsFolder, "underworld"));
} }
@Test @Test
@@ -146,6 +208,7 @@ public class PackDownloaderTest {
assertNotNull(result); assertNotNull(result);
assertTrue(result.changed()); assertTrue(result.changed());
assertTrue(result.restartRequired());
assertTrue(Files.isRegularFile(target.resolve("dimensions/underworld.json"))); assertTrue(Files.isRegularFile(target.resolve("dimensions/underworld.json")));
assertTrue(Files.isRegularFile(target.resolve("dimensions/underworld_roof.json"))); assertTrue(Files.isRegularFile(target.resolve("dimensions/underworld_roof.json")));
assertFalse(Files.exists(target.resolve("partial.txt"))); assertFalse(Files.exists(target.resolve("partial.txt")));
@@ -157,9 +220,9 @@ public class PackDownloaderTest {
assertFalse(PackDownloader.isDefaultOverworld("theend")); assertFalse(PackDownloader.isDefaultOverworld("theend"));
assertFalse(PackDownloader.isDefaultOverworld("")); assertFalse(PackDownloader.isDefaultOverworld(""));
assertFalse(PackDownloader.isDefaultOverworld(null)); assertFalse(PackDownloader.isDefaultOverworld(null));
assertFalse(PackDownloader.isManagedBetaPack("theend")); assertFalse(PackDownloader.isManagedPack("theend"));
assertFalse(PackDownloader.isManagedBetaPack("")); assertFalse(PackDownloader.isManagedPack(""));
assertFalse(PackDownloader.isManagedBetaPack(null)); assertFalse(PackDownloader.isManagedPack(null));
} }
@Test @Test
@@ -299,7 +362,7 @@ public class PackDownloaderTest {
assertEquals(feedback.toString(), "replaceable", result.key()); assertEquals(feedback.toString(), "replaceable", result.key());
assertTrue(result.changed()); assertTrue(result.changed());
assertFalse(result.restartRequired()); assertTrue(result.restartRequired());
assertEquals("new", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8)); assertEquals("new", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8));
assertFalse(Files.exists(target.toPath().resolve("old-only.txt"))); assertFalse(Files.exists(target.toPath().resolve("old-only.txt")));
assertTransactionStateClean(packsFolder); assertTransactionStateClean(packsFolder);
@@ -1,78 +0,0 @@
/*
* 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.core.service;
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 StudioSVCManagedBetaPackTest {
@Test
public void startupSelectsOnlyMissingManagedBetaPacks() throws IOException {
Path workspace = Files.createTempDirectory("iris-managed-beta-startup");
try {
assertEquals(
List.of("overworld", "underworld"),
StudioSVC.missingManagedBetaPacks(workspace.toFile())
);
createPack(workspace, "overworld");
assertEquals(
List.of("underworld"),
StudioSVC.missingManagedBetaPacks(workspace.toFile())
);
createDimension(workspace, "underworld", "underworld_roof");
assertEquals(
List.of("underworld"),
StudioSVC.missingManagedBetaPacks(workspace.toFile())
);
createPack(workspace, "underworld");
assertEquals(List.of(), StudioSVC.missingManagedBetaPacks(workspace.toFile()));
} finally {
deleteTree(workspace);
}
}
private static void createPack(Path workspace, String key) throws IOException {
createDimension(workspace, key, key);
}
private static void createDimension(Path workspace, String folder, String key) throws IOException {
Path dimensions = Files.createDirectories(workspace.resolve(folder).resolve("dimensions"));
Files.writeString(dimensions.resolve(key + ".json"), "{}", StandardCharsets.UTF_8);
}
private static void deleteTree(Path root) throws IOException {
try (Stream<Path> paths = Files.walk(root)) {
for (Path path : paths.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).toList()) {
Files.deleteIfExists(path);
}
}
}
}
@@ -0,0 +1,24 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StudioSVCPackDownloadContractTest {
@Test
public void downloadsRequireManualRestartWithoutMutatingLiveDatapacks() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/service/StudioSVC.java"));
int mutationStart = source.indexOf("private void runPackMutation(");
int mutationEnd = source.indexOf("private boolean finishStandalonePackMutation(", mutationStart);
int finishEnd = source.indexOf("private void runOffPrimaryThread(", mutationEnd);
String downloadMutation = source.substring(mutationStart, finishEnd);
assertTrue(downloadMutation.contains("Restart the server before using the downloaded Iris pack."));
assertFalse(downloadMutation.contains("ServerConfigurator.restart()"));
assertFalse(downloadMutation.contains("installDataPacksIfChanged"));
}
}