Iris downloader / History update

- Allow people to download in place and update in place the media
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-18 21:52:19 -04:00
1 parent b18681a3e2
commit edadd14f4b
40 files changed
+836 -122

No files matched your search

@@ -1465,9 +1465,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void printPacks() {
File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder();
for (String line : IrisSplashComposer.composePackLines(packFolder, Iris::reportError)) {
Iris.info(line);
}
IrisSplashComposer.composePackLines(packFolder, Iris::reportError).thenAccept(lines -> {
if (isEnabled()) {
lines.forEach(Iris::info);
}
});
}
public int getIrisVersion() {
@@ -21,13 +21,11 @@ package art.arcane.iris.command;
import com.google.gson.JsonObject;
import art.arcane.iris.Iris;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
import art.arcane.iris.world.runtime.ChunkClearer;
import art.arcane.iris.generation.validation.GoldenHashScanner;
import art.arcane.iris.world.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.generation.runtime.IrisEngineSVC;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.pack.IrisPackBenchmarking;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
@@ -68,7 +66,6 @@ import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.localization.BukkitRuntimeMessages;
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", descriptionKey = "iris.director.commanddeveloper.director.iris_world_manager", aliases = {"dev"})
public class CommandDeveloper implements DirectorExecutor {
@Director(description = "Get Loaded TectonicPlates Count", descriptionKey = "iris.director.commanddeveloper.director.get_loaded_tectonicplates_count", origin = DirectorOrigin.BOTH, sync = true)
@@ -184,27 +181,7 @@ public class CommandDeveloper implements DirectorExecutor {
@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
) {
if (!confirm) {
sender().sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING,
MessageArgument.untrusted("world", world.getName()),
MessageArgument.untrusted("pack", pack.getLoadKey())
));
return;
}
File folder = world.getWorldFolder();
folder.mkdirs();
try (LifecycleOperationCoordinator.Lease lease = LifecycleOperationCoordinator.get().acquire(
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
pack.getLoadKey()
)) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
} catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage());
}
CommandPack.stageWorldUpdate(sender(), world, pack, confirm);
}
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test")
@@ -437,7 +437,9 @@ public class CommandIris implements DirectorExecutor {
@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 = "link", description = "A direct HTTP or HTTPS zip link", defaultValue = NO_DOWNLOAD_SOURCE)
String link
String link,
@Param(name = "overwrite", description = "Back up and replace an installed pack", defaultValue = "false")
boolean overwrite
) {
String builtInPack = NO_DOWNLOAD_SOURCE.equals(pack) ? null : pack;
String directLink = NO_DOWNLOAD_SOURCE.equals(link) ? null : link;
@@ -446,14 +448,14 @@ public class CommandIris implements DirectorExecutor {
return;
}
if (builtInPack != null) {
Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack);
Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack, overwrite);
return;
}
if (!PackDownloader.isDirectZipUrl(directLink)) {
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL));
return;
}
Iris.service(StudioSVC.class).downloadUrl(sender(), directLink);
Iris.service(StudioSVC.class).downloadUrl(sender(), directLink, overwrite);
}
@Director(description = "Get metrics for your world", descriptionKey = "iris.director.commandiris.director.get_metrics_your_world", aliases = "measure", origin = DirectorOrigin.PLAYER)
@@ -27,6 +27,9 @@ import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.localization.C;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.command.handlers.DimensionHandler;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
@@ -35,6 +38,7 @@ import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.localization.TextKey;
import art.arcane.iris.spi.IrisPlatforms;
import org.bukkit.World;
import java.io.File;
import java.io.IOException;
@@ -48,6 +52,42 @@ import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.localization.BukkitRuntimeMessages;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
public class CommandPack implements DirectorExecutor {
@Director(name = "update-world", description = "Stage an installed pack for a world and request a clean restart", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update")
public void updateWorld(
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
World world,
@Param(description = "The installed dimension to use", descriptionKey = "iris.director.commanddeveloper.param.pack_install_into_world", contextual = true, contextualOverride = true, aliases = "dimension")
IrisDimension pack,
@Param(description = "Confirm that the complete world has been backed up", descriptionKey = "iris.director.commanddeveloper.param.make_sure_make_backup_read_warnings_first", defaultValue = "false", aliases = "c")
boolean confirm
) {
stageWorldUpdate(sender(), world, pack, confirm);
}
static void stageWorldUpdate(VolmitSender sender, World world, IrisDimension pack, boolean confirm) {
if (!IrisToolbelt.isIrisWorld(world) || IrisToolbelt.isIrisStudioWorld(world)) {
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_UPDATE_WORLD_REQUIRES_PERSISTENT_WORLD));
return;
}
if (!confirm) {
sender.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING,
MessageArgument.untrusted("world", world.getName()),
MessageArgument.untrusted("pack", pack.getLoadKey())
));
return;
}
try (LifecycleOperationCoordinator.Lease lease = LifecycleOperationCoordinator.get().acquire(
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
world.getName()
)) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender, pack, world.getWorldFolder(), world.getSeed());
} catch (LifecycleOperationCoordinator.BusyException e) {
sender.sendMessage(C.YELLOW + e.getMessage());
}
}
/**
* Director treats a blank defaultValue as "required", which made the all-packs branch
* unreachable from chat. The "*" sentinel keeps the parameter genuinely optional; a blank
@@ -15,22 +15,25 @@ import static org.junit.Assert.assertTrue;
public class CommandIrisDownloadContractTest {
@Test
public void commandExposesOnlyPackAndLinkParameters() throws NoSuchMethodException {
Method method = CommandIris.class.getDeclaredMethod("download", String.class, String.class);
public void commandRequiresExplicitOverwriteForPackReplacement() throws NoSuchMethodException {
Method method = CommandIris.class.getDeclaredMethod("download", String.class, String.class, boolean.class);
Parameter[] parameters = method.getParameters();
Param pack = parameters[0].getAnnotation(Param.class);
Param link = parameters[1].getAnnotation(Param.class);
Param overwrite = parameters[2].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(3, 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());
assertEquals("overwrite", overwrite.name());
assertEquals("false", overwrite.defaultValue());
}
@Test
@@ -0,0 +1,108 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.localization.BukkitRuntimeMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility;
import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.TextKey;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CommandPackUpdateWorldTest {
@Test
public void packNamespaceExposesAnExplicitConfirmation() throws Exception {
DirectorNodeDescriptor command = DirectorAnnotationCompatibility.fromMethod(
CommandPack.class.getDeclaredMethod("updateWorld", World.class, IrisDimension.class, boolean.class)
).orElseThrow();
assertEquals("update-world", command.getName());
assertTrue(command.getParameters().get(0).isRequired());
assertTrue(command.getParameters().get(1).isRequired());
assertFalse(command.getParameters().get(2).isRequired());
assertEquals("false", command.getParameters().get(2).getDefaultValue());
}
@Test
public void unconfirmedAndNonPersistentWorldUpdatesCannotPublish() {
VolmitSender sender = mock(VolmitSender.class);
World world = mock(World.class);
IrisDimension pack = new IrisDimension();
pack.setLoadKey("overworld");
when(world.getName()).thenReturn("example");
try (MockedStatic<Iris> iris = mockStatic(Iris.class);
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class)) {
CommandPack.stageWorldUpdate(sender, world, pack, true);
toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true);
toolbelt.when(() -> IrisToolbelt.isIrisStudioWorld(world)).thenReturn(true);
CommandPack.stageWorldUpdate(sender, world, pack, true);
toolbelt.when(() -> IrisToolbelt.isIrisStudioWorld(world)).thenReturn(false);
CommandPack.stageWorldUpdate(sender, world, pack, false);
iris.verifyNoInteractions();
verify(sender, times(2)).sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_PACK_UPDATE_WORLD_REQUIRES_PERSISTENT_WORLD));
verify(sender).sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING,
MessageArgument.untrusted("world", "example"),
MessageArgument.untrusted("pack", "overworld")));
}
}
@Test
public void updateResultsRenderTheWorldPackAndVersion() {
for (TextKey key : List.of(BukkitRuntimeMessages.COMMAND_PACK_UPDATE_WORLD_STAGED,
BukkitRuntimeMessages.COMMAND_PACK_UPDATE_WORLD_UNCHANGED)) {
String message = IrisLanguage.text(key,
MessageArgument.untrusted("world", "example"),
MessageArgument.untrusted("pack", "overworld"),
MessageArgument.untrusted("version", 4011));
assertTrue(message.contains("example"));
assertTrue(message.contains("overworld"));
assertTrue(message.contains("4011"));
assertFalse(message.contains("{world}"));
assertFalse(message.contains("{pack}"));
assertFalse(message.contains("{version}"));
}
}
@Test
public void confirmedUpdateUsesTheExistingWorldFolderAndSeed() {
VolmitSender sender = mock(VolmitSender.class);
World world = mock(World.class);
IrisDimension pack = new IrisDimension();
File folder = new File("example");
StudioSVC studio = mock(StudioSVC.class);
when(world.getName()).thenReturn("example");
when(world.getWorldFolder()).thenReturn(folder);
when(world.getSeed()).thenReturn(42L);
try (MockedStatic<Iris> iris = mockStatic(Iris.class);
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class)) {
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true);
CommandPack.stageWorldUpdate(sender, world, pack, true);
verify(studio).replaceIntoWorld(sender, pack, folder, 42L);
}
}
}