This commit is contained in:
Brian Neumann-Fopiano
2026-08-13 16:54:37 -04:00
parent 1586b4bd1a
commit 32beca236c
49 changed files with 311 additions and 1136 deletions
@@ -175,8 +175,8 @@ public final class IrisWorldGeneratorResolver {
+ " but its dimension failed to load; not redownloading. Fix or delete the pack folder.");
return null;
}
Iris.warn("Unable to find dimension type " + id + ". Install its pack with /iris download "
+ id + " and restart the server.");
Iris.warn("Unable to find dimension type " + id + ". Install its pack with "
+ PackDownloader.downloadCommandFor(id) + " and restart the server.");
}
return dimension;
@@ -197,9 +197,7 @@ public class CommandDeveloper implements DirectorExecutor {
@Param(description = "The pack to install into the world", descriptionKey = "iris.director.commanddeveloper.param.pack_install_into_world", contextual = true, aliases = "dimension")
IrisDimension pack,
@Param(description = "Make sure to make a backup & read the warnings first!", descriptionKey = "iris.director.commanddeveloper.param.make_sure_make_backup_read_warnings_first", defaultValue = "false", aliases = "c")
boolean confirm,
@Param(description = "Should Iris download the pack again for you", descriptionKey = "iris.director.commanddeveloper.param.should_iris_download_pack_again_you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
boolean freshDownload
boolean confirm
) {
if (!confirm) {
sender().sendMessage(IrisLanguage.text(
@@ -213,10 +211,6 @@ public class CommandDeveloper implements DirectorExecutor {
File folder = world.getWorldFolder();
folder.mkdirs();
if (freshDownload) {
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
}
try (LifecycleOperationCoordinator.Lease lease = LifecycleOperationCoordinator.get().acquire(
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
@@ -95,6 +95,7 @@ import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.RuntimeUiMessages;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command", descriptionKey = "iris.director.commandiris.director.basic_command")
public class CommandIris implements DirectorExecutor {
private static final String NO_DOWNLOAD_SOURCE = "__none__";
private static final long WORLD_UNLOAD_TIMEOUT_SECONDS = 150L;
private CommandStudio studio;
@@ -169,9 +170,10 @@ public class CommandIris implements DirectorExecutor {
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_COULD_NOT_FIND_DOWNLOAD_DIMENSION, MessageArgument.untrusted("resolvedType", resolvedType)));
sender().sendMessage("Could not find dimension '" + resolvedType + "'.");
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOWNLOAD_MANUALLY_IRIS_DOWNLOAD, MessageArgument.untrusted("resolvedType", resolvedType)));
sender().sendMessage("Install its pack with " + PackDownloader.downloadCommandFor(resolvedType)
+ " and restart the server.");
return;
}
@@ -703,25 +705,28 @@ public class CommandIris implements DirectorExecutor {
@Director(description = "Download a project.", descriptionKey = "iris.director.commandiris.director.download_project", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", aliases = "project")
@Param(name = "pack", description = "The built-in pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", defaultValue = NO_DOWNLOAD_SOURCE, customHandler = DownloadPackHandler.class)
String pack,
@Param(name = "branch", description = "The branch to download from", descriptionKey = "iris.director.commandiris.param.branch_download_from", defaultValue = PackDownloader.DEFAULT_BRANCH)
String branch,
@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
@Param(name = "link", description = "A direct HTTP or HTTPS zip link", defaultValue = NO_DOWNLOAD_SOURCE)
String link
) {
if (PackDownloader.isDirectZipUrl(pack)) {
sender().sendMessage("Downloading Iris pack from " + pack
+ (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 {
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);
String builtInPack = NO_DOWNLOAD_SOURCE.equals(pack) ? null : pack;
String directLink = NO_DOWNLOAD_SOURCE.equals(link) ? null : link;
if ((builtInPack == null) == (directLink == null)) {
sender().sendMessage("Use exactly one source: /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>.");
return;
}
if (builtInPack != null) {
sender().sendMessage("Downloading built-in Iris pack '" + builtInPack + "' from its beta release.");
Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack);
return;
}
if (!PackDownloader.isDirectZipUrl(directLink)) {
sender().sendMessage("Iris requires link= to contain a valid HTTP or HTTPS .zip URL.");
return;
}
sender().sendMessage("Downloading Iris pack from " + directLink + ".");
Iris.service(StudioSVC.class).downloadUrl(sender(), directLink);
}
@Director(description = "Get metrics for your world", descriptionKey = "iris.director.commandiris.director.get_metrics_your_world", aliases = "measure", origin = DirectorOrigin.PLAYER)
@@ -1063,6 +1068,35 @@ public class CommandIris implements DirectorExecutor {
}
}
public static class DownloadPackHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>(PackDownloader.builtInPacks());
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (NO_DOWNLOAD_SOURCE.equals(in)) {
return null;
}
String pack = in == null ? "" : in.trim().toLowerCase(Locale.ROOT);
if (!PackDownloader.isBuiltInPack(pack)) {
throw new DirectorParsingException("Pack must be 'overworld' or 'underworld'");
}
return pack;
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
static final class MainWorldPublication implements AutoCloseable {
private final Path target;
private boolean committed;
@@ -0,0 +1,45 @@
package art.arcane.iris.core.commands;
import art.arcane.volmlib.util.director.annotations.Param;
import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public class CommandIrisDownloadContractTest {
@Test
public void commandExposesOnlyPackAndLinkParameters() throws NoSuchMethodException {
Method method = CommandIris.class.getDeclaredMethod("download", String.class, String.class);
Parameter[] parameters = method.getParameters();
Param pack = parameters[0].getAnnotation(Param.class);
Param link = parameters[1].getAnnotation(Param.class);
List<Method> downloadMethods = Arrays.stream(CommandIris.class.getDeclaredMethods())
.filter((Method candidate) -> candidate.getName().equals("download"))
.toList();
assertEquals(1, downloadMethods.size());
assertEquals(2, downloadMethods.getFirst().getParameterCount());
assertEquals("pack", pack.name());
assertEquals(0L, Arrays.stream(pack.aliases()).filter((String alias) -> !alias.isBlank()).count());
assertEquals(CommandIris.DownloadPackHandler.class, pack.customHandler());
assertEquals("link", link.name());
assertEquals(0L, Arrays.stream(link.aliases()).filter((String alias) -> !alias.isBlank()).count());
}
@Test
public void packHandlerAcceptsOnlyTheTwoBuiltInPacks() throws Exception {
CommandIris.DownloadPackHandler handler = new CommandIris.DownloadPackHandler();
assertEquals("overworld", handler.parse("OVERWORLD", false));
assertEquals("underworld", handler.parse("underworld", false));
assertEquals(Arrays.asList("overworld", "underworld"), handler.getPossibilities());
assertNull(handler.parse("__none__", false));
assertThrows(Exception.class, () -> handler.parse("custom", false));
}
}
@@ -1,20 +0,0 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class CommandIrisDownloadDefaultTest {
@Test
public void downloadBranchParamDefaultsToTheCoreConstant() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/commands/CommandIris.java"));
int branchParam = source.indexOf("name = \"branch\"");
assertTrue("CommandIris must declare the branch param", branchParam >= 0);
String declaration = source.substring(branchParam, source.indexOf(')', branchParam));
assertTrue("branch param default must be PackDownloader.DEFAULT_BRANCH, not a literal",
declaration.contains("defaultValue = PackDownloader.DEFAULT_BRANCH"));
}
}
@@ -414,7 +414,7 @@ public final class ModdedForcedDatapack {
}
LOGGER.info("Iris forced startup datapack staged: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), stagingDirectory);
if (packCount == 0) {
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world.");
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack with /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>, then restart before creating an Iris world.");
}
}
@@ -1,107 +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 art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.PackDownloadMessages;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.volmlib.util.localization.MessageArgument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public final class ModdedPackInstaller {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Pattern PACK_NAME = Pattern.compile("[a-z0-9_-]+");
private static final Pattern BRANCH_NAME = Pattern.compile("[A-Za-z0-9._-]+");
private static final ConcurrentHashMap<String, Object> INSTALL_LOCKS = new ConcurrentHashMap<>();
private ModdedPackInstaller() {
}
public static boolean install(Path configDir, String pack, String branch,
boolean forceOverwrite, boolean refreshDatapack,
Consumer<String> feedback) {
if (pack == null || !PACK_NAME.matcher(pack).matches()) {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.INVALID_PACK_NAME, MessageArgument.untrusted("pack", String.valueOf(pack))));
return false;
}
boolean managed = PackDownloader.isManagedPack(pack);
if (!acceptsBranch(pack, branch)) {
feedback.accept(IrisLanguage.plain(PackDownloadMessages.INVALID_BRANCH_NAME, MessageArgument.untrusted("branch", String.valueOf(branch))));
return false;
}
Object installLock = INSTALL_LOCKS.computeIfAbsent(pack, key -> new Object());
synchronized (installLock) {
File packs = configDir.resolve("irisworldgen").resolve("packs").toFile();
try {
PackDownloader.PackInstallResult result = managed
? PackDownloader.downloadManaged(packs, pack, forceOverwrite, feedback)
: PackDownloader.download(
packs,
"IrisDimensions/" + pack,
branch,
forceOverwrite,
false,
pack,
feedback);
boolean installed = result != null;
if (shouldRefreshDatapack(result, refreshDatapack)) {
// Pack-install completion is one of the four forced-datapack regeneration triggers; every
// install call site already runs off the server thread, so regenerate inline here. A
// regeneration failure must never turn a successful install into a failed one.
try {
ModdedForcedDatapack.regenerateIfStale("pack install " + pack);
} catch (Throwable regenerationFailure) {
LOGGER.error("Iris installed pack '{}' but could not regenerate the forced datapack", pack, regenerationFailure);
}
}
if (result != null && result.restartRequired()) {
feedback.accept("Pack '" + pack + "' is installed on disk and requires a server restart before its active data changes.");
}
return installed;
} catch (IOException error) {
String source = managed ? "configured Git source" : "branch " + branch;
LOGGER.error("Iris pack download failed for IrisDimensions/{} ({})", pack, source, error);
feedback.accept(IrisLanguage.plain(
PackDownloadMessages.DOWNLOAD_FAILED,
MessageArgument.untrusted("type", error.getClass().getSimpleName()),
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(error))
));
return false;
}
}
}
static boolean acceptsBranch(String pack, String branch) {
return PackDownloader.isManagedPack(pack)
|| (branch != null && BRANCH_NAME.matcher(branch).matches());
}
static boolean shouldRefreshDatapack(PackDownloader.PackInstallResult result, boolean refreshDatapack) {
return result != null && result.changed() && refreshDatapack;
}
}
@@ -133,7 +133,7 @@ public final class ModdedStartup {
List<File> packDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
PackValidationRegistry.clear();
if (packDirs.isEmpty()) {
LOGGER.info("Iris found no packs to validate under {}; install one with /iris download <pack>",
LOGGER.info("Iris found no packs to validate under {}; install one with /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>",
packsRoot.getAbsolutePath());
return;
}
@@ -27,7 +27,6 @@ import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedForcedDatapack;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedServerLevels;
import art.arcane.iris.modded.ModdedWorldgenIds;
@@ -53,6 +52,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
@@ -258,54 +258,68 @@ public final class IrisModdedCommands {
return 1;
}
static int download(CommandSourceStack source, String pack,
String branch, boolean forceOverwrite) {
boolean managed = PackDownloader.isManagedPack(pack);
boolean directUrl = PackDownloader.isDirectZipUrl(pack);
String baseDownloadSource = directUrl ? "direct ZIP URL"
: managed ? "configured Git source" : "branch " + branch;
String downloadSource = forceOverwrite
? baseDownloadSource + IrisLanguage.plain(RuntimeUiMessages.DOWNLOAD_OVERWRITE_SUFFIX)
: baseDownloadSource;
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("downloadSource", downloadSource)));
static int download(CommandSourceStack source, String rawRequest) {
DownloadRequest request = parseDownloadRequest(rawRequest);
if (request == null) {
fail(source, "Use /iris download pack=overworld, /iris download pack=underworld, or /iris download link=<zip-url>.");
return 0;
}
String target = request.pack() == null ? request.url() : request.pack();
String downloadSource = request.pack() == null ? "direct ZIP URL" : "built-in beta release";
ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", target), MessageArgument.untrusted("downloadSource", downloadSource)));
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
MessageArgument.untrusted("pack", pack),
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource)));
return 0;
}
scheduler.async(() -> {
boolean installed;
if (directUrl) {
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)));
boolean installed = false;
File packs = ModdedPackCommands.packsRoot();
try {
PackDownloader.PackInstallResult result = request.pack() == null
? PackDownloader.downloadUrl(
packs,
request.url(),
false,
(String message) -> scheduler.global(() -> ok(source, message))
)
: PackDownloader.downloadBuiltIn(
packs,
request.pack(),
false,
(String message) -> scheduler.global(() -> ok(source, message))
);
installed = result != null;
} catch (IOException | RuntimeException error) {
LOGGER.error("Iris pack download failed for {}", target, error);
}
if (installed) {
scheduler.global(() -> ok(source, "Pack installed on disk. Restart the server before using it."));
} 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", target), MessageArgument.untrusted("downloadSource", downloadSource))));
}
});
return 1;
}
static DownloadRequest parseDownloadRequest(String rawRequest) {
if (rawRequest == null || rawRequest.isBlank()) {
return null;
}
if (rawRequest.startsWith("pack=")) {
String pack = rawRequest.substring("pack=".length()).trim().toLowerCase(Locale.ROOT);
return PackDownloader.isBuiltInPack(pack) ? new DownloadRequest(pack, null) : null;
}
if (rawRequest.startsWith("link=")) {
String url = rawRequest.substring("link=".length()).trim();
return PackDownloader.isDirectZipUrl(url) ? new DownloadRequest(null, url) : null;
}
return null;
}
static int metrics(CommandSourceStack source) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
@@ -371,4 +385,7 @@ public final class IrisModdedCommands {
static void fail(CommandSourceStack source, String message) {
ModdedCommandFeedback.fail(source, message);
}
record DownloadRequest(String pack, String url) {
}
}
@@ -70,7 +70,7 @@ final class ModdedCommandHelp {
Entry.command("seed", "", ModdedHelpMessages.COMMAND_SEED_PRINT_WORLD_AND_ENGINE_SEED_INFORMATION),
Entry.command("debug", "", ModdedHelpMessages.COMMAND_DEBUG_TOGGLE_IRIS_DEBUG_LOGGING_AND_SAVE_SETTINGS_JSON),
Entry.command("reload", "", ModdedHelpMessages.COMMAND_RELOAD_RELOAD_SETTINGS_JSON_ALSO_HOTLOADED_AUTOMATICALLY_EVERY_3S),
Entry.command("download", "<pack> [branch] [overwrite]", ModdedHelpMessages.COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT, "dl"),
Entry.command("download", "<pack=overworld|pack=underworld|link=<zip-url>>", ModdedHelpMessages.COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT, "dl"),
Entry.command("metrics", "", ModdedHelpMessages.COMMAND_METRICS_PRINT_GENERATION_METRICS_FOR_YOUR_CURRENT_IRIS_DIMENSION, "measure"),
Entry.command("regen", "[radius]", ModdedHelpMessages.COMMAND_REGEN_DELETE_AND_REGENERATE_NEARBY_CHUNKS_IN_PLACE, "rg"),
Entry.group("pregen", ModdedHelpMessages.GROUP_PREGEN_PREGENERATE_AN_IRIS_DIMENSION, "pregenerate"),
@@ -18,8 +18,6 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.pack.PackDownloader;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -27,15 +25,24 @@ import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.resources.Identifier;
import java.util.List;
import java.util.function.Predicate;
final class ModdedCommandTree {
private static final SuggestionProvider<CommandSourceStack> DOWNLOAD_SOURCES =
(context, builder) ->
SharedSuggestionProvider.suggest(
List.of("pack=overworld", "pack=underworld", "link="),
builder
);
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
/**
* SP-4: read-only inspection must work for an unopped player in a no-cheats singleplayer world, where the
@@ -189,35 +196,11 @@ final class ModdedCommandTree {
private static LiteralArgumentBuilder<CommandSourceStack> downloadTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH, false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH, true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), PackDownloader.DEFAULT_BRANCH,
BoolArgumentType.getBool(context, "overwrite"))))
.then(Commands.argument("branch", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"), false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"), true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"),
BoolArgumentType.getBool(context, "overwrite"))))));
.then(Commands.argument("source", StringArgumentType.greedyString()).suggests(DOWNLOAD_SOURCES)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.download(
context.getSource(),
StringArgumentType.getString(context, "source")
)));
}
private static LiteralArgumentBuilder<CommandSourceStack> metricsTree(String name) {
@@ -23,7 +23,6 @@ import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.StructurePackageClosure;
import art.arcane.iris.core.project.IrisProjectCopier;
import art.arcane.iris.engine.framework.Engine;
@@ -40,7 +39,6 @@ import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
@@ -385,13 +383,9 @@ public final class ModdedStudioCommands {
try {
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_MISSING_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
if (!installed || !new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_TRY_IRIS_DOWNLOAD, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack))));
return;
}
server.execute(() -> IrisModdedCommands.fail(source, "Pack '" + pack
+ "' is not installed. Use /iris download pack=overworld or pack=underworld, then restart."));
return;
}
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(pack);
@@ -593,13 +587,9 @@ public final class ModdedStudioCommands {
try {
File templateFolder = new File(packsRoot, template);
if (!new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), template, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
if (!installed || !new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_TEMPLATE_COULD_NOT_BE_DOWNLOADED_INSTALL_PACK_WITH_DIMENSIONS_JSON, MessageArgument.untrusted("template", template), MessageArgument.untrusted("template2", template))));
return;
}
server.execute(() -> IrisModdedCommands.fail(source, "Template pack '" + template
+ "' is not installed. Install its zip with /iris download link=<zip-url>, then restart."));
return;
}
IrisProjectCopier.copyProject(templateFolder, target, template, name);
try {
@@ -20,7 +20,6 @@ package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -28,7 +27,6 @@ import art.arcane.iris.modded.MainWorldService;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedModConfig;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.modded.ModdedPrimaryWorldRouter;
import art.arcane.iris.modded.ModdedServerLevels;
import art.arcane.iris.modded.ModdedStartup;
@@ -176,21 +174,9 @@ public final class ModdedWorldCommands {
if (packFolder.isDirectory()) {
return enableInstalled(source, server, dimensionId, pack, packDimension, seed);
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
server.execute(() -> {
if (!installed || !packFolder.isDirectory()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
return;
}
enableInstalled(source, server, dimensionId, pack, packDimension, seed);
});
}, "Iris World Pack Download");
thread.setDaemon(true);
thread.start();
return 1;
IrisModdedCommands.fail(source, "Pack '" + pack
+ "' is not installed. Use /iris download pack=overworld or pack=underworld, then restart.");
return 0;
}
private static int enableInstalled(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, String packDimension, long seed) {
@@ -279,21 +265,9 @@ public final class ModdedWorldCommands {
if (packFolder.isDirectory()) {
return applyMainWorld(source, pack, packDimension, packRaw, seed);
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_INSTALLED_DOWNLOADING_IRISDIMENSIONS_2, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, PackDownloader.DEFAULT_BRANCH, false, true,
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
server.execute(() -> {
if (!installed || !packFolder.isDirectory()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_COULD_NOT_BE_DOWNLOADED_CHECK_NAME_INSTALL_IT_WITH_2, MessageArgument.untrusted("pack", pack), MessageArgument.untrusted("pack2", pack)));
return;
}
applyMainWorld(source, pack, packDimension, packRaw, seed);
});
}, "Iris Main World Pack Download");
thread.setDaemon(true);
thread.start();
return 1;
IrisModdedCommands.fail(source, "Pack '" + pack
+ "' is not installed. Use /iris download pack=overworld or pack=underworld, then restart.");
return 0;
}
private static int applyMainWorld(CommandSourceStack source, String pack, String packDimension, String packRef, long seed) {
@@ -1,31 +0,0 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedDownloadBranchParityTest {
private static final List<String> DOWNLOAD_SOURCES = List.of(
"art/arcane/iris/modded/command/ModdedWorldCommands.java",
"art/arcane/iris/modded/command/ModdedStudioCommands.java",
"art/arcane/iris/modded/command/ModdedCommandTree.java");
@Test
public void implicitAndExplicitDownloadsShareTheCoreDefaultBranch() throws Exception {
for (String source : DOWNLOAD_SOURCES) {
Path path = Path.of(System.getProperty("iris.moddedCommonSources"), source);
String text = Files.readString(path);
assertFalse(source + " must not hardcode a \"master\" download branch",
text.contains("\"master\""));
assertFalse(source + " must not hardcode a \"stable\" download branch",
text.contains("\"stable\""));
assertTrue(source + " must use PackDownloader.DEFAULT_BRANCH",
text.contains("PackDownloader.DEFAULT_BRANCH"));
}
}
}
@@ -1,52 +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 art.arcane.iris.core.pack.PackDownloader;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedPackInstallerTest {
@Test
public void managedPacksIgnoreTheRequestedBranch() {
assertTrue(ModdedPackInstaller.acceptsBranch("overworld", null));
assertTrue(ModdedPackInstaller.acceptsBranch("underworld", "feature/arbitrary"));
}
@Test
public void nonManagedPacksStillRequireAValidBranch() {
assertTrue(ModdedPackInstaller.acceptsBranch("custom", "stable"));
assertFalse(ModdedPackInstaller.acceptsBranch("custom", null));
assertFalse(ModdedPackInstaller.acceptsBranch("custom", "feature/arbitrary"));
}
@Test
public void startupBatchDefersDatapackRefresh() {
PackDownloader.PackInstallResult changed = new PackDownloader.PackInstallResult("underworld", true, true);
assertFalse(ModdedPackInstaller.shouldRefreshDatapack(changed, false));
assertTrue(ModdedPackInstaller.shouldRefreshDatapack(changed, true));
assertFalse(ModdedPackInstaller.shouldRefreshDatapack(
new PackDownloader.PackInstallResult("underworld", false, false),
true
));
}
}
@@ -10,7 +10,9 @@ import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisModdedCommandParityTest {
@@ -50,17 +52,34 @@ public class IrisModdedCommandParityTest {
child(studio, "pkg");
CommandNode<CommandSourceStack> download = child(iris, "download");
CommandNode<CommandSourceStack> pack = child(download, "pack");
child(pack, "force");
child(pack, "overwrite");
CommandNode<CommandSourceStack> branch = child(pack, "branch");
child(branch, "force");
child(branch, "overwrite");
CommandNode<CommandSourceStack> source = child(download, "source");
assertTrue(source.getChildren().isEmpty());
assertSame(iris, child(dispatcher.getRoot(), "ir").getRedirect());
assertSame(iris, child(dispatcher.getRoot(), "irs").getRedirect());
}
@Test
public void downloadRequestAcceptsOnlyBuiltInsAndZipLinks() {
IrisModdedCommands.DownloadRequest overworld = IrisModdedCommands.parseDownloadRequest("pack=overworld");
IrisModdedCommands.DownloadRequest underworld = IrisModdedCommands.parseDownloadRequest("pack=UNDERWORLD");
IrisModdedCommands.DownloadRequest link = IrisModdedCommands.parseDownloadRequest(
"link=https://packs.example.test/custom.zip?token=a=b"
);
assertNotNull(overworld);
assertEquals("overworld", overworld.pack());
assertNotNull(underworld);
assertEquals("underworld", underworld.pack());
assertNotNull(link);
assertEquals("https://packs.example.test/custom.zip?token=a=b", link.url());
assertNull(IrisModdedCommands.parseDownloadRequest("overworld"));
assertNull(IrisModdedCommands.parseDownloadRequest("pack=custom"));
assertNull(IrisModdedCommands.parseDownloadRequest("link=https://packs.example.test/custom.tar.gz"));
assertNull(IrisModdedCommands.parseDownloadRequest("pack=underworld branch=stable"));
assertNull(IrisModdedCommands.parseDownloadRequest("pack=underworld overwrite=true"));
}
@Test
public void helpDocumentsParityCommandsAndPlatformStubs() {
assertTrue(ModdedCommandHelp.documents("what", "here"));