diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index 25ab06965..f3fc8ab3f 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -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() { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java index d763722d4..997dc4c12 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandDeveloper.java @@ -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") diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java index 271ab1c91..f3c15c99b 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandIris.java @@ -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) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPack.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPack.java index 839ad8bf4..601b4a115 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPack.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandPack.java @@ -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 diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandIrisDownloadContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandIrisDownloadContractTest.java index 1ab071080..14308fac1 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandIrisDownloadContractTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandIrisDownloadContractTest.java @@ -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 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 diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandPackUpdateWorldTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandPackUpdateWorldTest.java new file mode 100644 index 000000000..088fa815c --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandPackUpdateWorldTest.java @@ -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 = mockStatic(Iris.class); + MockedStatic 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 = mockStatic(Iris.class); + MockedStatic 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); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java index e356feab8..62d46eeba 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java @@ -40,9 +40,8 @@ public final class ModdedIrisSplash { private static void printPacks(ModdedLoader loader) { File packFolder = IrisPlatforms.get().packsFolderNoCreate(); - for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) { - IrisLogging.info(line); - } + IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError) + .thenAccept(lines -> lines.forEach(IrisLogging::info)); } private static void printLogo(ModdedLoader loader) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java index 0c2795b97..e74291cb2 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java @@ -406,14 +406,14 @@ public final class IrisModdedCommands { ? PackDownloader.downloadUrl( packs, request.url(), - false, + request.overwrite(), (String message) -> dispatchDownloadFeedback(source, () -> ok(source, message)), cancellation ) : PackDownloader.downloadBuiltIn( packs, request.pack(), - false, + request.overwrite(), (String message) -> dispatchDownloadFeedback(source, () -> ok(source, message)), cancellation ); @@ -478,15 +478,33 @@ public final class IrisModdedCommands { 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; + String pack = null; + String url = null; + Boolean overwrite = null; + for (String argument : rawRequest.trim().split("\\s+")) { + if (argument.startsWith("pack=") && pack == null) { + pack = argument.substring("pack=".length()).toLowerCase(Locale.ROOT); + if (!PackDownloader.isBuiltInPack(pack)) { + return null; + } + } else if (argument.startsWith("link=") && url == null) { + url = argument.substring("link=".length()); + if (!PackDownloader.isDirectZipUrl(url)) { + return null; + } + } else if (argument.startsWith("overwrite=") && overwrite == null) { + String value = argument.substring("overwrite=".length()); + if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) { + return null; + } + overwrite = Boolean.parseBoolean(value); + } else { + return null; + } } - if (rawRequest.startsWith("link=")) { - String url = rawRequest.substring("link=".length()).trim(); - return PackDownloader.isDirectZipUrl(url) ? new DownloadRequest(null, url) : null; - } - return null; + return (pack == null) == (url == null) + ? null + : new DownloadRequest(pack, url, Boolean.TRUE.equals(overwrite)); } static int metrics(CommandSourceStack source) { @@ -555,6 +573,6 @@ public final class IrisModdedCommands { ModdedCommandFeedback.fail(source, message); } - record DownloadRequest(String pack, String url) { + record DownloadRequest(String pack, String url, boolean overwrite) { } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java index 4c4dd4456..e7e93426b 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -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", ">", ModdedHelpMessages.COMMAND_DOWNLOAD_DOWNLOAD_A_PACK_PROJECT, "dl"), + Entry.command("download", "> [overwrite=true]", 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"), diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java index 327aa72df..555e785d4 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandTree.java @@ -43,7 +43,7 @@ final class ModdedCommandTree { private static final SuggestionProvider DOWNLOAD_SOURCES = (context, builder) -> SharedSuggestionProvider.suggest( - List.of("pack=overworld", "pack=underworld", "link="), + List.of("pack=overworld", "pack=underworld", "link=", "pack=overworld overwrite=true", "pack=underworld overwrite=true"), builder ); private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java index c4bff9bd2..c42ae4518 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/IrisModdedCommandParityTest.java @@ -89,7 +89,27 @@ public class IrisModdedCommandParityTest { 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")); + assertFalse(overworld.overwrite()); + } + + @Test + public void downloadOverwriteRequiresOneSourceAndAnExplicitBoolean() { + IrisModdedCommands.DownloadRequest pack = IrisModdedCommands.parseDownloadRequest("pack=underworld overwrite=true"); + IrisModdedCommands.DownloadRequest link = IrisModdedCommands.parseDownloadRequest( + "overwrite=true link=https://packs.example.test/custom.zip?token=a=b"); + + assertNotNull(pack); + assertTrue(pack.overwrite()); + assertEquals("underworld", pack.pack()); + assertNotNull(link); + assertTrue(link.overwrite()); + assertEquals("https://packs.example.test/custom.zip?token=a=b", link.url()); + assertFalse(IrisModdedCommands.parseDownloadRequest("pack=overworld overwrite=false").overwrite()); + assertNull(IrisModdedCommands.parseDownloadRequest("overwrite=true")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=overworld overwrite=yes")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=overworld overwrite=true overwrite=false")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=overworld pack=underworld")); + assertNull(IrisModdedCommands.parseDownloadRequest("pack=overworld link=https://packs.example.test/custom.zip")); } @Test diff --git a/core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashComposer.java b/core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashComposer.java index 9af62b5f5..cbf3f9bcf 100644 --- a/core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashComposer.java +++ b/core/src/main/java/art/arcane/iris/diagnostics/splash/IrisSplashComposer.java @@ -1,12 +1,15 @@ package art.arcane.iris.diagnostics.splash; import art.arcane.iris.BuildConstants; +import art.arcane.iris.pack.BuiltInPackUpdates; import java.io.File; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; public final class IrisSplashComposer { private static final String SPLASH_PADDING = " ".repeat(4); @@ -40,8 +43,14 @@ public final class IrisSplashComposer { return builder.toString(); } - public static List composePackLines(File packFolder, IrisSplashPackScanner.SplashPackErrorReporter reporter) { + public static CompletableFuture> composePackLines(File packFolder, IrisSplashPackScanner.SplashPackErrorReporter reporter) { List packs = IrisSplashPackScanner.collect(packFolder, reporter); + return BuiltInPackUpdates.check(packs.stream().map(IrisSplashPackScanner.SplashPackMetadata::name).toList()) + .thenApply(updates -> composePackLines(packs, updates)); + } + + static List composePackLines(List packs, + Map updates) { if (packs.isEmpty()) { return List.of(); } @@ -49,7 +58,15 @@ public final class IrisSplashComposer { List lines = new ArrayList<>(packs.size() + 1); lines.add("Custom Dimensions: " + packs.size()); for (IrisSplashPackScanner.SplashPackMetadata pack : packs) { - lines.add(" " + pack.name() + " v" + pack.version()); + BuiltInPackUpdates.Update update = updates.get(pack.name()); + lines.add(" " + pack.name() + " v" + pack.version() + + (update == null ? "" : update.suffix(pack.version()))); + } + for (IrisSplashPackScanner.SplashPackMetadata pack : packs) { + BuiltInPackUpdates.Update update = updates.get(pack.name()); + if (update != null && update.newerThan(pack.version())) { + lines.add("Update " + pack.name() + ": /iris download pack=" + pack.name() + " overwrite=true"); + } } return lines; } diff --git a/core/src/main/java/art/arcane/iris/localization/BukkitRuntimeMessages.java b/core/src/main/java/art/arcane/iris/localization/BukkitRuntimeMessages.java index b11584402..6b5e949df 100644 --- a/core/src/main/java/art/arcane/iris/localization/BukkitRuntimeMessages.java +++ b/core/src/main/java/art/arcane/iris/localization/BukkitRuntimeMessages.java @@ -180,6 +180,18 @@ public final class BukkitRuntimeMessages { C.RED + "To stage the update and request a clean restart:", C.RED + "/iris developer update-world {world} {pack} confirm=true" ); + public static final TextKey COMMAND_PACK_UPDATE_WORLD_REQUIRES_PERSISTENT_WORLD = TextKey.of( + "iris.bukkit.runtime.commandpack.update_world_requires_persistent_world", + C.RED + "Choose a loaded Iris world. Studio worlds cannot receive generation updates." + ); + public static final TextKey COMMAND_PACK_UPDATE_WORLD_STAGED = TextKey.of( + "iris.bukkit.runtime.commandpack.update_world_staged", + C.GREEN + "Staged {pack} v{version} for world '{world}'. A clean restart will activate the pack with terrain blending." + ); + public static final TextKey COMMAND_PACK_UPDATE_WORLD_UNCHANGED = TextKey.of( + "iris.bukkit.runtime.commandpack.update_world_unchanged", + C.GREEN + "World '{world}' already uses {pack} v{version} with the same generation settings. No restart is needed." + ); public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW = TextKey.of( "iris.bukkit.runtime.bulkstructureimporter.importing_vanilla_datapack_structures_mode_includenonjigsaw", C.GREEN + "Importing " + C.WHITE + "{total}" + C.GREEN + " vanilla & datapack structures (mode=" + "{mode}" + ", includeNonJigsaw=" + "{includeNonJigsaw}" + ")..." @@ -658,6 +670,9 @@ public final class BukkitRuntimeMessages { COMMAND_PACK_COMPAT_NONE, COMMAND_PACK_COMPAT_REMEDY, COMMAND_DEVELOPER_UPDATE_WORLD_WARNING, + COMMAND_PACK_UPDATE_WORLD_REQUIRES_PERSISTENT_WORLD, + COMMAND_PACK_UPDATE_WORLD_STAGED, + COMMAND_PACK_UPDATE_WORLD_UNCHANGED, BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW, BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY, BULK_STRUCTURE_IMPORTER_JIGSAW, diff --git a/core/src/main/java/art/arcane/iris/localization/PackDownloadMessages.java b/core/src/main/java/art/arcane/iris/localization/PackDownloadMessages.java index 2e0551cea..61fb9223e 100644 --- a/core/src/main/java/art/arcane/iris/localization/PackDownloadMessages.java +++ b/core/src/main/java/art/arcane/iris/localization/PackDownloadMessages.java @@ -39,7 +39,7 @@ public final class PackDownloadMessages { ); public static final TextKey PROGRESS_UNCHANGED = TextKey.of( "iris.runtime.pack_download.progress.unchanged", - C.YELLOW + "Iris pack '{pack}' is already installed." + C.YELLOW + "Iris pack '{pack}' is already installed. Use overwrite=true to back up and replace it." ); public static final TextKey PROGRESS_FAILED = TextKey.of( "iris.runtime.pack_download.progress.failed", @@ -99,6 +99,14 @@ public final class PackDownloadMessages { "iris.runtime.pack_download.shutting_down", C.YELLOW + "Iris is shutting down and is not accepting pack downloads." ); + public static final TextKey CLOSE_STUDIO = TextKey.of( + "iris.runtime.pack_download.close_studio", + "Close the active Studio project before replacing an installed pack." + ); + public static final TextKey BACKUP_RETAINED = TextKey.of( + "iris.runtime.pack_download.backup_retained", + "Previous pack saved to {path}." + ); public static final TextKey DOWNLOADING = TextKey.of( "iris.runtime.pack_download.downloading", "Downloading {url}" @@ -150,7 +158,7 @@ public final class PackDownloadMessages { ); public static final TextKey PACK_KEY_CONFLICT = TextKey.of( "iris.runtime.pack_download.pack_key_conflict", - "Another pack is using the key {key}. Import failed!" + "Pack {key} is already installed. Use overwrite=true to back up and replace it." ); public static final TextKey ACQUIRED = TextKey.of( "iris.runtime.pack_download.acquired", @@ -158,7 +166,7 @@ public final class PackDownloadMessages { ); public static final TextKey ALREADY_INSTALLED = TextKey.of( "iris.runtime.pack_download.already_installed", - "Pack {key} is already installed, skipping download." + "Pack {key} is already installed, skipping download. Use overwrite=true to back up and replace it." ); public static final TextKey IN_PROGRESS = TextKey.of( "iris.runtime.pack_download.in_progress", @@ -207,6 +215,8 @@ public final class PackDownloadMessages { INVALID_URL, INVALID_BUILT_IN, SHUTTING_DOWN, + CLOSE_STUDIO, + BACKUP_RETAINED, DOWNLOADING, UNPACKING, UNPACK_FAILED, diff --git a/core/src/main/java/art/arcane/iris/pack/AtomicDirectoryPublisher.java b/core/src/main/java/art/arcane/iris/pack/AtomicDirectoryPublisher.java index 885ff5bc7..ac68f37bf 100644 --- a/core/src/main/java/art/arcane/iris/pack/AtomicDirectoryPublisher.java +++ b/core/src/main/java/art/arcane/iris/pack/AtomicDirectoryPublisher.java @@ -71,7 +71,7 @@ public final class AtomicDirectoryPublisher { public static final class Publication implements AutoCloseable { private final Path target; - private final Path backup; + private Path backup; private boolean committed; private boolean closed; @@ -87,6 +87,22 @@ public final class AtomicDirectoryPublisher { committed = true; } + public synchronized boolean retainBackup(Path destination) throws IOException { + if (closed || committed) { + throw new IllegalStateException("Directory publication is already finalized."); + } + if (backup == null) { + return false; + } + Path retained = Objects.requireNonNull(destination, "destination").toAbsolutePath().normalize(); + if (Files.exists(retained) || Files.isSymbolicLink(retained)) { + throw new FileAlreadyExistsException(retained.toString()); + } + move(backup, retained); + backup = retained; + return true; + } + public synchronized void cleanupBackup() throws IOException { if (!committed) { throw new IllegalStateException("Directory publication is not committed."); diff --git a/core/src/main/java/art/arcane/iris/pack/BuiltInPackUpdates.java b/core/src/main/java/art/arcane/iris/pack/BuiltInPackUpdates.java new file mode 100644 index 000000000..756901e70 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/pack/BuiltInPackUpdates.java @@ -0,0 +1,112 @@ +package art.arcane.iris.pack; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.regex.Pattern; + +public final class BuiltInPackUpdates { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(5); + private static final Pattern VERSION = Pattern.compile("[0-9]+"); + private static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(REQUEST_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + private BuiltInPackUpdates() { + } + + public static CompletableFuture> check(List packs) { + return check(packs, CLIENT, pack -> URI.create( + "https://api.github.com/repos/IrisDimensions/" + pack + "/releases/latest")); + } + + static CompletableFuture> check(List packs, HttpClient client, + Function endpoint) { + Map> requests = new LinkedHashMap<>(); + for (String pack : packs) { + if (!PackDownloader.isBuiltInPack(pack) || requests.containsKey(pack)) { + continue; + } + HttpRequest request = HttpRequest.newBuilder(endpoint.apply(pack)) + .timeout(REQUEST_TIMEOUT) + .header("Accept", "application/vnd.github+json") + .header("User-Agent", "Iris-Pack-Updates") + .GET() + .build(); + CompletableFuture> response = client.sendAsync(request, + HttpResponse.BodyHandlers.limiting(HttpResponse.BodyHandlers.ofString(), 1024L * 1024L)); + requests.put(pack, response.copy().orTimeout(REQUEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((result, error) -> { + if (error != null) { + response.cancel(true); + } + }) + .thenApply(result -> readRelease(pack, result)) + .exceptionally(error -> Update.unavailable())); + } + return CompletableFuture.allOf(requests.values().toArray(CompletableFuture[]::new)) + .thenApply(ignored -> { + Map updates = new LinkedHashMap<>(); + requests.forEach((pack, request) -> updates.put(pack, request.join())); + return Map.copyOf(updates); + }); + } + + private static Update readRelease(String pack, HttpResponse response) { + if (response.statusCode() != 200) { + return Update.unavailable(); + } + JsonObject release = JsonParser.parseString(response.body()).getAsJsonObject(); + if (release.get("draft").getAsBoolean() || release.get("prerelease").getAsBoolean()) { + return Update.unavailable(); + } + String version = release.get("tag_name").getAsString().replaceFirst("^[vV]\\+?", ""); + if (!VERSION.matcher(version).matches()) { + return Update.unavailable(); + } + for (JsonElement element : release.getAsJsonArray("assets")) { + JsonObject asset = element.getAsJsonObject(); + if ((pack + ".zip").equals(asset.get("name").getAsString())) { + return new Update(version); + } + } + return Update.unavailable(); + } + + public record Update(String version) { + public static Update unavailable() { + return new Update(null); + } + + public boolean newerThan(String installedVersion) { + return version != null && VERSION.matcher(installedVersion).matches() + && new BigInteger(version).compareTo(new BigInteger(installedVersion)) > 0; + } + + public String suffix(String installedVersion) { + if (version == null) { + return " (update check unavailable)"; + } + if (!VERSION.matcher(installedVersion).matches()) { + return " (latest v" + version + ")"; + } + return newerThan(installedVersion) + ? " -> v" + version + " available" + : ""; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/pack/PackDownloader.java b/core/src/main/java/art/arcane/iris/pack/PackDownloader.java index 88c135962..35707b114 100644 --- a/core/src/main/java/art/arcane/iris/pack/PackDownloader.java +++ b/core/src/main/java/art/arcane/iris/pack/PackDownloader.java @@ -646,16 +646,36 @@ public final class PackDownloader { } IrisData.getLoaded(new File(packsFolder, prepared.key())).ifPresent(IrisData::close); IrisData.getLoaded(target.toFile()).ifPresent(IrisData::close); - try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) { - publication.commit(); - try { - publication.cleanupBackup(); - } catch (IOException exception) { - IrisLogging.reportError( - "Pack '" + prepared.key() + "' was published, but its transaction backup could not be cleaned.", - exception - ); + Path retainedBackup = null; + if (forceOverwrite && Files.exists(target)) { + Path backupRoot = packsRoot.resolve(".backups"); + if (Files.isSymbolicLink(backupRoot)) { + throw new IOException("Pack backup folder is an unsafe symbolic link: " + backupRoot); } + Files.createDirectories(backupRoot); + retainedBackup = backupRoot.resolve(prepared.key() + "-" + UUID.randomUUID()); + } + try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) { + if (retainedBackup != null) { + publication.retainBackup(retainedBackup); + } + publication.commit(); + if (retainedBackup == null) { + try { + publication.cleanupBackup(); + } catch (IOException exception) { + IrisLogging.reportError( + "Pack '" + prepared.key() + "' was published, but its transaction backup could not be cleaned.", + exception + ); + } + } + } + if (retainedBackup != null) { + sendFeedback(feedback, IrisLanguage.plain( + PackDownloadMessages.BACKUP_RETAINED, + MessageArgument.untrusted("path", retainedBackup.toString()) + )); } PackValidationRegistry.publish(prepared.validation()); sendValidationFeedback(prepared.validation(), feedback); diff --git a/core/src/main/java/art/arcane/iris/studio/StudioSVC.java b/core/src/main/java/art/arcane/iris/studio/StudioSVC.java index 5bba92d88..9cd32ec81 100644 --- a/core/src/main/java/art/arcane/iris/studio/StudioSVC.java +++ b/core/src/main/java/art/arcane/iris/studio/StudioSVC.java @@ -290,6 +290,16 @@ public class StudioSVC implements IrisService { ? history.packRoot(history.pendingActivation().orElseThrow().activationId()) : history.activePackRoot(); IrisDimension installed = loadInstalledDimension(installedPack, source, dimensionKey); + if (stageUpdate) { + sender.sendMessage(IrisLanguage.text( + restartRequired + ? BukkitRuntimeMessages.COMMAND_PACK_UPDATE_WORLD_STAGED + : BukkitRuntimeMessages.COMMAND_PACK_UPDATE_WORLD_UNCHANGED, + MessageArgument.untrusted("world", dimensionRoot.getName()), + MessageArgument.untrusted("pack", installed.getLoadKey()), + MessageArgument.untrusted("version", installed.getVersion()) + )); + } if (restartRequired) { ServerConfigurator.restart("An Iris generation epoch update is pending activation."); } @@ -352,6 +362,7 @@ public class StudioSVC implements IrisService { packRoot, GenerationPackFingerprint.CURRENT_VERSION ); + validatePublishedPack(packRoot); IrisData data = IrisData.openDatapackCompiler(packRoot.toFile()); try { IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false); @@ -378,6 +389,10 @@ public class StudioSVC implements IrisService { fingerprint, aliasPolicy ); + if (!fingerprint.equals(GenerationPackFingerprint.compute( + packRoot, GenerationPackFingerprint.CURRENT_VERSION))) { + throw new IOException("Generation pack changed during validation: " + packRoot); + } return new GenerationCandidate(fingerprint, dimensionContract, registryContract); } finally { data.close(); @@ -628,14 +643,15 @@ public class StudioSVC implements IrisService { return replaceIntoPackDirectory(sender, dimension, folder); } - public void downloadBuiltIn(VolmitSender sender, String key) { + public void downloadBuiltIn(VolmitSender sender, String key, boolean overwrite) { if (!PackDownloader.isBuiltInPack(key)) { sender.sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_BUILT_IN)); return; } PackDownloadProgressReporter reporter = new PackDownloadProgressReporter(sender, key); runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, key, reporter, cancellation -> { - PackDownloader.PackInstallResult result = downloadBuiltInLocked(key, cancellation, reporter); + requireClosedStudioForPackOverwrite(overwrite); + PackDownloader.PackInstallResult result = downloadBuiltInLocked(key, overwrite, cancellation, reporter); if (result == null) { reporter.fail(null); return; @@ -645,7 +661,7 @@ public class StudioSVC implements IrisService { }, "Failed to download built-in Iris pack '" + key + "'."); } - public void downloadUrl(VolmitSender sender, String url) { + public void downloadUrl(VolmitSender sender, String url, boolean overwrite) { if (!PackDownloader.isDirectZipUrl(url)) { sender.sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL)); return; @@ -656,10 +672,11 @@ public class StudioSVC implements IrisService { url ); runPackMutation(sender, LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD, "remote-zip", reporter, cancellation -> { + requireClosedStudioForPackOverwrite(overwrite); PackDownloader.PackInstallResult result = PackDownloader.downloadUrl( getWorkspaceFolder(), url, - false, + overwrite, reporter::detail, cancellation, reporter @@ -675,23 +692,30 @@ public class StudioSVC implements IrisService { private PackDownloader.PackInstallResult downloadBuiltInLocked( String expectedKey, + boolean overwrite, PackDownloader.DownloadCancellation cancellation, PackDownloadProgressReporter reporter ) throws IOException { - if (PackDownloader.isBuiltInPackPresent(getWorkspaceFolder(), expectedKey)) { + if (!overwrite && PackDownloader.isBuiltInPackPresent(getWorkspaceFolder(), expectedKey)) { return new PackDownloader.PackInstallResult(expectedKey, false, false); } return PackDownloader.downloadBuiltIn( getWorkspaceFolder(), expectedKey, - false, + overwrite, reporter::detail, cancellation, reporter ); } + private void requireClosedStudioForPackOverwrite(boolean overwrite) throws IOException { + if (overwrite && activeProject != null) { + throw new IOException(IrisLanguage.plain(PackDownloadMessages.CLOSE_STUDIO)); + } + } + public boolean isProjectOpen() { return activeProject != null && activeProject.isOpen(); } diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index fd3c6ae2e..13788a1c7 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -1577,6 +1577,10 @@ failed_fragment = " fehlgeschlagen {failed}" "commandpack.compat_header" = "§ePack '{pack}': Inhalte in Minecraft {version} nicht verfügbar" "commandpack.compat_none" = "§aPack '{pack}' verwendet keine Inhalte, die in Minecraft {version} fehlen." "commandpack.compat_remedy" = "§7Aktualisiere den Server auf ein neueres Minecraft, um diese Inhalte wiederherzustellen, oder deklariere Ersatzwerte (Dimension blockFallbacks, Block backup)." +"commandpack.update_world_requires_persistent_world" = "§cWähle eine geladene Iris-Welt. Studio-Welten können keine Generierungsaktualisierungen erhalten." +"commandpack.update_world_staged" = "§a{pack} v{version} für Welt '{world}' vorgemerkt. Ein sauberer Neustart aktiviert das Pack mit Geländeübergängen." +"commandpack.update_world_unchanged" = "§aWelt '{world}' verwendet bereits {pack} v{version} mit denselben Generierungseinstellungen. Kein Neustart erforderlich." + [iris.runtime.studio] opening = "§6Studio §bÖFFNET" @@ -1688,7 +1692,7 @@ chunk_failed = "Chunk {x},{z} FEHLGESCHLAGEN: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris-Pack '{pack}' installiert§8 | §f{transferred}§7 in §f{elapsed}" -"progress.unchanged" = "§eIris-Pack '{pack}' ist bereits installiert." +"progress.unchanged" = "§eIris-Pack '{pack}' ist bereits installiert. Verwende overwrite=true, um es zu sichern und zu ersetzen." "progress.failed" = "§cDownload des Iris-Packs fehlgeschlagen.§7 Prüfe die Download-Details oben und versuche es erneut." "progress.failed_detail" = "§cDownload des Iris-Packs fehlgeschlagen.§7 {error}" "progress.cancelled" = "§eDownload des Iris-Packs vor der Veröffentlichung abgebrochen." @@ -1714,14 +1718,17 @@ check_github = "Prüfe, ob die Datei auf GitHub vorhanden ist, und melde das Pro invalid_dimension = "Ungültiger Dimensionsordner unter dimensions/." importing = "{name} ({key}) wird importiert" dimension_key_conflict = "Eine andere Dimension im Pack-Ordner verwendet bereits den Schlüssel {key}. Import fehlgeschlagen!" -pack_key_conflict = "Ein anderer Pack verwendet bereits den Schlüssel {key}. Import fehlgeschlagen!" +"pack_key_conflict" = "Iris-Pack '{key}' ist bereits installiert. Verwende overwrite=true, um es zu sichern und zu ersetzen." acquired = "{name} erfolgreich abgerufen." -already_installed = "Pack {key} ist bereits installiert, Download wird übersprungen." +"already_installed" = "Pack {key} ist bereits installiert, Download wird übersprungen. Verwende overwrite=true, um es zu sichern und zu ersetzen." in_progress = "Ein anderer Iris-Pack-Download läuft bereits. Warte, bis er abgeschlossen ist, bevor du es erneut versuchst." validation_failed = "Pack '{pack}' hat die Validierung nicht bestanden; Welt- und Studio-Erstellung werden verweigert. Gründe:" validation_reason = " - {reason}" validated_with_warnings = { other = "Pack '{pack}' mit {count} Warnungen validiert.", one = "Pack '{pack}' mit {count} Warnung validiert." } validated = "Pack '{pack}' validiert." +"close_studio" = "Schließe das aktive Studio-Projekt, bevor du ein installiertes Pack ersetzt." +"backup_retained" = "Vorheriges Pack unter {path} gesichert." + [iris.client.vision] title = "Iris Vision" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index aa4a1d004..81b488d75 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -1577,6 +1577,10 @@ failed_fragment = " fallidos {failed}" "commandpack.compat_header" = "§ePack '{pack}': contenido no disponible en Minecraft {version}" "commandpack.compat_none" = "§aEl pack '{pack}' no usa contenido que falte en Minecraft {version}." "commandpack.compat_remedy" = "§7Actualiza el servidor a un Minecraft más reciente para recuperar este contenido, o declara alternativas (blockFallbacks de la dimensión, backup del bloque)." +"commandpack.update_world_requires_persistent_world" = "§cElige un mundo de Iris cargado. Los mundos de Studio no pueden recibir actualizaciones de generación." +"commandpack.update_world_staged" = "§a{pack} v{version} preparado para el mundo '{world}'. Un reinicio limpio activará el pack con transición de terreno." +"commandpack.update_world_unchanged" = "§aEl mundo '{world}' ya usa {pack} v{version} con los mismos ajustes de generación. No hace falta reiniciar." + [iris.runtime.studio] opening = "§6Studio §bABRIENDO" @@ -1688,7 +1692,7 @@ chunk_failed = "Chunk {x},{z} FALLÓ: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aEl pack de Iris '{pack}' está instalado§8 | §f{transferred}§7 en §f{elapsed}" -"progress.unchanged" = "§eEl pack de Iris '{pack}' ya está instalado." +"progress.unchanged" = "§eEl pack de Iris '{pack}' ya está instalado. Usa overwrite=true para crear una copia de seguridad y reemplazarlo." "progress.failed" = "§cLa descarga del pack de Iris ha fallado.§7 Revisa los detalles de la descarga de arriba y vuelve a intentarlo." "progress.failed_detail" = "§cLa descarga del pack de Iris ha fallado.§7 {error}" "progress.cancelled" = "§eLa descarga del pack de Iris se canceló antes de publicarlo." @@ -1714,14 +1718,17 @@ check_github = "¡Comprueba que esté presente en GitHub e informa de ello al pe invalid_dimension = "Carpeta de dimensión no válida en dimensions/." importing = "Importando {name} ({key})" dimension_key_conflict = "Otra dimensión de la carpeta packs ya usa la clave {key}. ¡La importación falló!" -pack_key_conflict = "Otro pack usa la clave {key}. ¡La importación falló!" +"pack_key_conflict" = "El pack de Iris '{key}' ya está instalado. Usa overwrite=true para crear una copia de seguridad y reemplazarlo." acquired = "{name} se obtuvo correctamente." -already_installed = "El pack {key} ya está instalado, se omite la descarga." +"already_installed" = "El pack {key} ya está instalado, se omite la descarga. Usa overwrite=true para crear una copia de seguridad y reemplazarlo." in_progress = "Ya hay otra descarga de un pack de Iris en curso. Espera a que termine antes de volver a intentarlo." validation_failed = "El pack '{pack}' no superó la validación; se rechazará la creación de mundos y de Studio. Motivos:" validation_reason = " - {reason}" validated_with_warnings = { one = "El pack '{pack}' se validó con {count} advertencia.", other = "El pack '{pack}' se validó con {count} advertencias." } validated = "El pack '{pack}' se validó." +"close_studio" = "Cierra el proyecto de Studio activo antes de reemplazar un pack instalado." +"backup_retained" = "Pack anterior guardado en {path}." + [iris.client.vision] title = "Iris Vision" diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml index fa35bf6c0..c466ef4c3 100644 --- a/core/src/main/resources/languages/fi_FI.toml +++ b/core/src/main/resources/languages/fi_FI.toml @@ -1582,6 +1582,10 @@ failed_fragment = " epäonnistui {failed}" "commandpack.compat_header" = "§ePaketti '{pack}': sisältö ei ole käytettävissä Minecraft {version} -versiossa" "commandpack.compat_none" = "§aPaketti '{pack}' ei käytä sisältöä, joka puuttuisi Minecraft {version} -versiosta." "commandpack.compat_remedy" = "§7Päivitä palvelin uudempaan Minecraft-versioon palauttaaksesi tämän sisällön tai määritä varasisältö (ulottuvuuden blockFallbacks, lohkon backup)." +"commandpack.update_world_requires_persistent_world" = "§cValitse ladattu Iris-maailma. Studio-maailmat eivät voi vastaanottaa generointipäivityksiä." +"commandpack.update_world_staged" = "§a{pack} v{version} valmisteltu maailmalle '{world}'. Hallittu uudelleenkäynnistys ottaa paketin ja maaston häivytyksen käyttöön." +"commandpack.update_world_unchanged" = "§aMaailma '{world}' käyttää jo pakettia {pack} v{version} samoilla generointiasetuksilla. Uudelleenkäynnistystä ei tarvita." + [iris.runtime.studio] opening = "§6Studio §bAVATAAN" @@ -1693,7 +1697,7 @@ chunk_failed = "Liha {x},{z} epäonnistui: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris-paketti '{pack}' asennettu§8 | §f{transferred}§7 ajassa §f{elapsed}" -"progress.unchanged" = "§eIris-paketti '{pack}' on jo asennettu." +"progress.unchanged" = "§eIris-paketti '{pack}' on jo asennettu. Varmuuskopioi ja korvaa se käyttämällä overwrite=true." "progress.failed" = "§cIris-paketin lataus epäonnistui.§7 Tarkista yllä olevat lataustiedot ja yritä uudelleen." "progress.failed_detail" = "§cIris-paketin lataus epäonnistui.§7 {error}" "progress.cancelled" = "§eIris-paketin lataus peruutettiin ennen julkaisua." @@ -1719,14 +1723,17 @@ check_github = "Tarkista, että se on läsnä GitHub Ja raportoikaa tästä henk invalid_dimension = "Virheellinen ulottuvuuskansio alla dimensions/." importing = "Tuo {name} ({key})" dimension_key_conflict = "Toinen ulottuvuus pakkauskansiossa jo käyttää avainta {key}. Tuonti epäonnistui!" -pack_key_conflict = "Toinen pakkaus käyttää avainta {key}. Tuonti epäonnistui!" +"pack_key_conflict" = "Iris-paketti '{key}' on jo asennettu. Varmuuskopioi ja korvaa se käyttämällä overwrite=true." acquired = "Onnistunut hankinta {name}." -already_installed = "Pack {key} on jo asennettu, lataus ohitetaan." +"already_installed" = "Pack {key} on jo asennettu, lataus ohitetaan. Varmuuskopioi ja korvaa se käyttämällä overwrite=true." in_progress = "Toinen Iris-paketin lataus on jo käynnissä. Odota sen valmistumista ennen kuin yrität uudelleen." validation_failed = "Pakkaus{pack}' Epäonnistunut validointi; maailma ja Studio luominen hylätään. Perusteet:" validation_reason = " - {reason}" validated_with_warnings = { other = "Pakkaus{pack}\" validoitu {count} varoitukset.", one = "Pakkaus{pack}\" validoitu {count} Varoitus." } validated = "Pakkaus{pack}' validoitu." +"close_studio" = "Sulje aktiivinen Studio-projekti ennen asennetun paketin korvaamista." +"backup_retained" = "Edellinen paketti tallennettu polkuun {path}." + [iris.client.vision] title = "Iris Näkö" diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml index ead5f8100..8221ccf45 100644 --- a/core/src/main/resources/languages/fr_FR.toml +++ b/core/src/main/resources/languages/fr_FR.toml @@ -1577,6 +1577,10 @@ failed_fragment = " échecs {failed}" "commandpack.compat_header" = "§ePack '{pack}' : contenu indisponible sur Minecraft {version}" "commandpack.compat_none" = "§aLe pack '{pack}' n'utilise aucun contenu absent de Minecraft {version}." "commandpack.compat_remedy" = "§7Mettez le serveur à jour vers un Minecraft plus récent pour restaurer ce contenu, ou déclarez des replis (blockFallbacks de la dimension, backup du bloc)." +"commandpack.update_world_requires_persistent_world" = "§cChoisissez un monde Iris chargé. Les mondes Studio ne peuvent pas recevoir de mises à jour de génération." +"commandpack.update_world_staged" = "§a{pack} v{version} préparé pour le monde '{world}'. Un redémarrage propre activera le pack avec une transition du terrain." +"commandpack.update_world_unchanged" = "§aLe monde '{world}' utilise déjà {pack} v{version} avec les mêmes paramètres de génération. Aucun redémarrage nécessaire." + [iris.runtime.studio] opening = "§6Studio §bOUVERTURE" @@ -1688,7 +1692,7 @@ chunk_failed = "Chunk {x},{z} ÉCHEC : {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aLe pack Iris '{pack}' est installé§8 | §f{transferred}§7 en §f{elapsed}" -"progress.unchanged" = "§eLe pack Iris '{pack}' est déjà installé." +"progress.unchanged" = "§eLe pack Iris '{pack}' est déjà installé. Utilisez overwrite=true pour le sauvegarder et le remplacer." "progress.failed" = "§cÉchec du téléchargement du pack Iris.§7 Consultez les détails du téléchargement ci-dessus et réessayez." "progress.failed_detail" = "§cÉchec du téléchargement du pack Iris.§7 {error}" "progress.cancelled" = "§eTéléchargement du pack Iris annulé avant sa publication." @@ -1714,14 +1718,17 @@ check_github = "Vérifiez qu'il est présent sur GitHub et signalez-le à l'équ invalid_dimension = "Dossier de dimension non valide dans dimensions/." importing = "Importation de {name} ({key})" dimension_key_conflict = "Une autre dimension du dossier packs utilise déjà la clé {key}. Échec de l'importation !" -pack_key_conflict = "Un autre pack utilise la clé {key}. Échec de l'importation !" +"pack_key_conflict" = "Le pack Iris '{key}' est déjà installé. Utilisez overwrite=true pour le sauvegarder et le remplacer." acquired = "{name} obtenu avec succès." -already_installed = "Le pack {key} est déjà installé, téléchargement ignoré." +"already_installed" = "Le pack {key} est déjà installé, téléchargement ignoré. Utilisez overwrite=true pour le sauvegarder et le remplacer." in_progress = "Un autre téléchargement de pack Iris est déjà en cours. Attendez qu’il se termine avant de réessayer." validation_failed = "Le pack '{pack}' a échoué à la validation ; la création de mondes et de Studio sera refusée. Raisons :" validation_reason = " - {reason}" validated_with_warnings = { one = "Le pack '{pack}' a été validé avec {count} avertissement.", other = "Le pack '{pack}' a été validé avec {count} avertissements." } validated = "Le pack '{pack}' a été validé." +"close_studio" = "Fermez le projet Studio actif avant de remplacer un pack installé." +"backup_retained" = "Ancien pack sauvegardé dans {path}." + [iris.client.vision] title = "Iris Vision" diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml index de46a6162..8590b4f5c 100644 --- a/core/src/main/resources/languages/he_IL.toml +++ b/core/src/main/resources/languages/he_IL.toml @@ -1582,6 +1582,10 @@ failed_fragment = " נכשל {failed}" "commandpack.compat_header" = "§eחבילה '{pack}': תוכן לא זמין ב-Minecraft {version}" "commandpack.compat_none" = "§aהחבילה '{pack}' אינה משתמשת בתוכן שחסר ב-Minecraft {version}." "commandpack.compat_remedy" = "§7עדכן את השרת ל-Minecraft חדש יותר כדי לשחזר את התוכן הזה, או הגדר חלופות (blockFallbacks של הממד, backup של הבלוק)." +"commandpack.update_world_requires_persistent_world" = "§cיש לבחור עולם Iris טעון. עולמות Studio אינם יכולים לקבל עדכוני יצירת עולם." +"commandpack.update_world_staged" = "§a{pack} v{version} הוכן עבור העולם '{world}'. הפעלה מחדש מסודרת תפעיל את החבילה עם שילוב פני השטח." +"commandpack.update_world_unchanged" = "§aהעולם '{world}' כבר משתמש בחבילה {pack} v{version} עם אותן הגדרות יצירה. אין צורך בהפעלה מחדש." + [iris.runtime.studio] opening = "§6Studio §bנפתח" @@ -1693,7 +1697,7 @@ chunk_failed = "צ'אנק {x},{z} נכשל: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aחבילת Iris '{pack}' הותקנה§8 | §f{transferred}§7 בתוך §f{elapsed}" -"progress.unchanged" = "§eחבילת Iris '{pack}' כבר מותקנת." +"progress.unchanged" = "§eחבילת Iris '{pack}' כבר מותקנת. יש להשתמש ב־overwrite=true כדי לגבות ולהחליף אותה." "progress.failed" = "§cהורדת חבילת Iris נכשלה.§7 יש לעיין בפרטי ההורדה שלעיל ולנסות שוב." "progress.failed_detail" = "§cהורדת חבילת Iris נכשלה.§7 {error}" "progress.cancelled" = "§eהורדת חבילת Iris בוטלה לפני הפרסום." @@ -1719,14 +1723,17 @@ check_github = "בדוק כי הוא נוכח GitHub לדווח על זה לצו invalid_dimension = "תיקיית מימד לא חוקית תחת dimensions/." importing = "יבוא {name} ({key})" dimension_key_conflict = "מימד נוסף בתיקיה של הלהקה כבר משתמש המפתח {key}. ייבוא נכשל!" -pack_key_conflict = "חבילה נוספת משתמשת במפתח {key}. ייבוא נכשל!" +"pack_key_conflict" = "חבילת Iris '{key}' כבר מותקנת. יש להשתמש ב־overwrite=true כדי לגבות ולהחליף אותה." acquired = "נרכשה בהצלחה {name}." -already_installed = "החבילה {key} כבר מותקנת, ההורדה מדולגת." +"already_installed" = "החבילה {key} כבר מותקנת, ההורדה מדולגת. יש להשתמש ב־overwrite=true כדי לגבות ולהחליף אותה." in_progress = "הורדה אחרת של חבילת Iris כבר מתבצעת. יש להמתין לסיומה לפני ניסיון נוסף." validation_failed = "Pack »{pack}\"התאימות הכושל; יצירת העולם והסטודיו לא תסרב. סיבות:" validation_reason = " - {reason}" validated_with_warnings = { other = "Pack »{pack}\"תאמת עם {count} אזהרות", one = "Pack »{pack}\"תאמת עם {count} אזהרה." } validated = "Pack »{pack}\"מאומת." +"close_studio" = "יש לסגור את פרויקט Studio הפעיל לפני החלפת חבילה מותקנת." +"backup_retained" = "החבילה הקודמת נשמרה בנתיב {path}." + [iris.client.vision] title = "Iris חזון" diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml index 6874ca343..20442dc97 100644 --- a/core/src/main/resources/languages/it_IT.toml +++ b/core/src/main/resources/languages/it_IT.toml @@ -1577,6 +1577,10 @@ failed_fragment = " non riusciti {failed}" "commandpack.compat_header" = "§ePack '{pack}': contenuti non disponibili su Minecraft {version}" "commandpack.compat_none" = "§aIl pack '{pack}' non usa contenuti assenti da Minecraft {version}." "commandpack.compat_remedy" = "§7Aggiorna il server a un Minecraft più recente per ripristinare questi contenuti, oppure dichiara delle alternative (blockFallbacks della dimensione, backup del blocco)." +"commandpack.update_world_requires_persistent_world" = "§cScegli un mondo Iris caricato. I mondi Studio non possono ricevere aggiornamenti della generazione." +"commandpack.update_world_staged" = "§a{pack} v{version} preparato per il mondo '{world}'. Un riavvio regolare attiverà il pacchetto con raccordo del terreno." +"commandpack.update_world_unchanged" = "§aIl mondo '{world}' usa già {pack} v{version} con le stesse impostazioni di generazione. Non serve riavviare." + [iris.runtime.studio] opening = "§6Studio §bAPERTURA" @@ -1688,7 +1692,7 @@ chunk_failed = "Chunk {x},{z} NON RIUSCITO: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aPack Iris '{pack}' installato§8 | §f{transferred}§7 in §f{elapsed}" -"progress.unchanged" = "§eIl pack Iris '{pack}' è già installato." +"progress.unchanged" = "§eIl pack Iris '{pack}' è già installato. Usa overwrite=true per crearne una copia di sicurezza e sostituirlo." "progress.failed" = "§cDownload del pack Iris non riuscito.§7 Controlla i dettagli del download qui sopra e riprova." "progress.failed_detail" = "§cDownload del pack Iris non riuscito.§7 {error}" "progress.cancelled" = "§eDownload del pack Iris annullato prima della pubblicazione." @@ -1714,14 +1718,17 @@ check_github = "Verifica che sia presente su GitHub e segnala il problema allo s invalid_dimension = "Cartella di dimensione non valida in dimensions/." importing = "Importazione {name} ({key})" dimension_key_conflict = "Un'altra dimensione nella cartella dei Pack usa già la chiave {key}. Importazione non riuscita!" -pack_key_conflict = "Un altro Pack usa già la chiave {key}. Importazione non riuscita!" +"pack_key_conflict" = "Il pack Iris '{key}' è già installato. Usa overwrite=true per crearne una copia di sicurezza e sostituirlo." acquired = "{name} acquisito correttamente." -already_installed = "Il pack {key} è già installato, download saltato." +"already_installed" = "Il pack {key} è già installato, download saltato. Usa overwrite=true per crearne una copia di sicurezza e sostituirlo." in_progress = "È già in corso il download di un altro pack Iris. Attendi che termini prima di riprovare." validation_failed = "Il Pack '{pack}' non ha superato la convalida; la creazione di mondi e Studio verrà rifiutata. Motivi:" validation_reason = " - {reason}" validated_with_warnings = { other = "Pack '{pack}' convalidato con {count} avvisi.", one = "Pack '{pack}' convalidato con {count} avviso." } validated = "Pack '{pack}' convalidato." +"close_studio" = "Chiudi il progetto Studio attivo prima di sostituire un pacchetto installato." +"backup_retained" = "Pacchetto precedente salvato in {path}." + [iris.client.vision] title = "Iris Vision" diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml index e8a3f4465..5b2238192 100644 --- a/core/src/main/resources/languages/ja-JP.toml +++ b/core/src/main/resources/languages/ja-JP.toml @@ -1580,6 +1580,10 @@ failed_fragment = " 失敗 {failed}" "commandpack.compat_header" = "§eパック '{pack}': Minecraft {version} では利用できない内容があります" "commandpack.compat_none" = "§aパック '{pack}' に Minecraft {version} で欠けている内容はありません。" "commandpack.compat_remedy" = "§7この内容を復元するにはサーバーを新しい Minecraft に更新するか、代替を宣言してください (ディメンションの blockFallbacks、ブロックの backup)。" +"commandpack.update_world_requires_persistent_world" = "§c読み込み済みの Iris ワールドを選択してください。Studio ワールドには生成の更新を適用できません。" +"commandpack.update_world_staged" = "§aワールド '{world}' 用に {pack} v{version} を準備しました。正常な再起動後、地形をなじませながら新しいパックが有効になります。" +"commandpack.update_world_unchanged" = "§aワールド '{world}' は同じ生成設定の {pack} v{version} を使用しています。再起動は不要です。" + [iris.runtime.studio] opening = "§6Studio §bオープン中" @@ -1691,7 +1695,7 @@ chunk_failed = "チャンク {x},{z} 失敗: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris パック '{pack}' をインストールしました§8 | §f{transferred}§7、所要時間 §f{elapsed}" -"progress.unchanged" = "§eIris パック '{pack}' はすでにインストールされています。" +"progress.unchanged" = "§eIris パック '{pack}' はすでにインストールされています。 バックアップして置き換えるには overwrite=true を指定してください。" "progress.failed" = "§cIris パックのダウンロードに失敗しました。§7 上記のダウンロード詳細を確認して、もう一度お試しください。" "progress.failed_detail" = "§cIris パックのダウンロードに失敗しました。§7 {error}" "progress.cancelled" = "§eIris パックのダウンロードは公開前にキャンセルされました。" @@ -1717,14 +1721,17 @@ check_github = "GitHub に存在することを確認し、スタッフへ報告 invalid_dimension = "dimensions/ 内のディメンションフォルダーが無効です。" importing = "{name}({key})をインポートしています" dimension_key_conflict = "packs フォルダー内の別のディメンションがキー {key} をすでに使用しています。インポートに失敗しました!" -pack_key_conflict = "別のパックがキー {key} を使用しています。インポートに失敗しました!" +"pack_key_conflict" = "Iris パック '{key}' はすでにインストールされています。 バックアップして置き換えるには overwrite=true を指定してください。" acquired = "{name} を取得しました。" -already_installed = "パック {key} は既にインストールされているため、ダウンロードをスキップします。" +"already_installed" = "パック {key} は既にインストールされているため、ダウンロードをスキップします。 バックアップして置き換えるには overwrite=true を指定してください。" in_progress = "別の Iris パックのダウンロードがすでに進行中です。完了してからもう一度お試しください。" validation_failed = "パック '{pack}' は検証に失敗しました。ワールドと Studio の作成を拒否します。理由:" validation_reason = " - {reason}" validated_with_warnings = { other = "パック '{pack}' の検証が {count} 件の警告付きで完了しました。", one = "パック '{pack}' の検証が {count} 件の警告付きで完了しました。" } validated = "パック '{pack}' の検証が完了しました。" +"close_studio" = "インストール済みのパックを置き換える前に、開いている Studio プロジェクトを閉じてください。" +"backup_retained" = "以前のパックを {path} に保存しました。" + [iris.client.vision] title = "Iris ビジョン" diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml index 0b0fd7be9..642f341b8 100644 --- a/core/src/main/resources/languages/ko_KR.toml +++ b/core/src/main/resources/languages/ko_KR.toml @@ -1580,6 +1580,10 @@ failed_fragment = " 실패한 {failed}" "commandpack.compat_header" = "§e팩 '{pack}': Minecraft {version} 에서 사용할 수 없는 콘텐츠" "commandpack.compat_none" = "§a팩 '{pack}' 은 Minecraft {version} 에 없는 콘텐츠를 사용하지 않습니다." "commandpack.compat_remedy" = "§7이 콘텐츠를 복원하려면 서버를 최신 Minecraft 로 업데이트하거나 대체를 선언하세요 (차원의 blockFallbacks, 블록의 backup)." +"commandpack.update_world_requires_persistent_world" = "§c로드된 Iris 월드를 선택하세요. Studio 월드는 생성 업데이트를 받을 수 없습니다." +"commandpack.update_world_staged" = "§a월드 '{world}'에 사용할 {pack} v{version}을 준비했습니다. 정상적으로 재시작하면 지형 혼합과 함께 팩이 활성화됩니다." +"commandpack.update_world_unchanged" = "§a월드 '{world}'는 이미 같은 생성 설정으로 {pack} v{version}을 사용 중입니다. 재시작할 필요가 없습니다." + [iris.runtime.studio] opening = "§6Studio §b여는 중" @@ -1691,7 +1695,7 @@ chunk_failed = "주 메뉴 {x},{z} 실패: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris 팩 '{pack}' 설치 완료§8 | §f{transferred}§7, 소요 시간 §f{elapsed}" -"progress.unchanged" = "§eIris 팩 '{pack}'은(는) 이미 설치되어 있습니다." +"progress.unchanged" = "§eIris 팩 '{pack}'은(는) 이미 설치되어 있습니다. 백업하고 교체하려면 overwrite=true를 사용하세요." "progress.failed" = "§cIris 팩 다운로드에 실패했습니다.§7 위의 다운로드 세부 정보를 확인한 후 다시 시도하세요." "progress.failed_detail" = "§cIris 팩 다운로드에 실패했습니다.§7 {error}" "progress.cancelled" = "§eIris 팩 다운로드가 게시 전에 취소되었습니다." @@ -1717,14 +1721,17 @@ check_github = "GitHub에 있는지 확인하고 직원에게 보고하세요!" invalid_dimension = "잘못된 차원 폴더 dimensions/." importing = "관련 상품 {name} ({key})" dimension_key_conflict = "packs 폴더의 다른 차원이 이미 키 {key}을(를) 사용합니다. 가져오기에 실패했습니다!" -pack_key_conflict = "다른 팩이 키 {key}을(를) 사용합니다. 가져오기에 실패했습니다!" +"pack_key_conflict" = "Iris 팩 '{key}'은(는) 이미 설치되어 있습니다. 백업하고 교체하려면 overwrite=true를 사용하세요." acquired = "성공적으로 취득 {name}." -already_installed = "팩 {key}이(가) 이미 설치되어 있어 다운로드를 건너뜁니다." +"already_installed" = "팩 {key}이(가) 이미 설치되어 있어 다운로드를 건너뜁니다. 백업하고 교체하려면 overwrite=true를 사용하세요." in_progress = "다른 Iris 팩 다운로드가 이미 진행 중입니다. 완료될 때까지 기다린 후 다시 시도하세요." validation_failed = "팩 '{pack}' 유효성 검사; 세계 및 스튜디오 생성은 거부됩니다. 이유:" validation_reason = " - {reason}" validated_with_warnings = { other = "팩 '{pack}' 유효성 {count} 경고.", one = "팩 '{pack}' 유효성 {count} 경고." } validated = "팩 '{pack}' 유효성." +"close_studio" = "설치된 팩을 교체하기 전에 활성 Studio 프로젝트를 닫으세요." +"backup_retained" = "이전 팩을 {path}에 저장했습니다." + [iris.client.vision] title = "Iris Vision" diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml index 0ccd94e8d..af191d941 100644 --- a/core/src/main/resources/languages/lt_LT.toml +++ b/core/src/main/resources/languages/lt_LT.toml @@ -1582,6 +1582,10 @@ failed_fragment = " nepavyko {failed}" "commandpack.compat_header" = "§ePaketas '{pack}': turinys nepasiekiamas Minecraft {version}" "commandpack.compat_none" = "§aPaketas '{pack}' nenaudoja turinio, kurio trūksta Minecraft {version}." "commandpack.compat_remedy" = "§7Atnaujinkite serverį į naujesnį Minecraft, kad atkurtumėte šį turinį, arba paskelbkite atsargas (dimensijos blockFallbacks, bloko backup)." +"commandpack.update_world_requires_persistent_world" = "§cPasirinkite įkeltą Iris pasaulį. Studio pasauliams negalima taikyti generavimo atnaujinimų." +"commandpack.update_world_staged" = "§aPasauliui '{world}' paruoštas {pack} v{version}. Tvarkingai paleidus iš naujo, paketas bus aktyvuotas su reljefo perėjimais." +"commandpack.update_world_unchanged" = "§aPasaulis '{world}' jau naudoja {pack} v{version} su tais pačiais generavimo nustatymais. Paleisti iš naujo nereikia." + [iris.runtime.studio] opening = "§6Studio §bATIDAROMA" @@ -1693,7 +1697,7 @@ chunk_failed = "Šriftas {x},{z} nepavyko: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§a„Iris“ paketas '{pack}' įdiegtas§8 | §f{transferred}§7 per §f{elapsed}" -"progress.unchanged" = "§e„Iris“ paketas '{pack}' jau įdiegtas." +"progress.unchanged" = "§e„Iris“ paketas '{pack}' jau įdiegtas. Naudokite overwrite=true, kad sukurtumėte atsarginę kopiją ir jį pakeistumėte." "progress.failed" = "§cNepavyko atsisiųsti „Iris“ paketo.§7 Peržiūrėkite aukščiau pateiktą atsisiuntimo informaciją ir bandykite dar kartą." "progress.failed_detail" = "§cNepavyko atsisiųsti „Iris“ paketo.§7 {error}" "progress.cancelled" = "§e„Iris“ paketo atsisiuntimas atšauktas prieš publikavimą." @@ -1719,14 +1723,17 @@ check_github = "Patikrinkite, ar jis yra GitHub ir pranešti apie tai darbuotoja invalid_dimension = "Netinkamas dimensijos aplankas pagal dimensions/." importing = "Importuojama {name} ({key})" dimension_key_conflict = "Kitas matmuo paketų aplanke jau naudoja raktą {key}. Importuoti nepavyko!" -pack_key_conflict = "Kita pakuotė naudoja raktą {key}. Importuoti nepavyko!" +"pack_key_conflict" = "„Iris“ paketas '{key}' jau įdiegtas. Naudokite overwrite=true, kad sukurtumėte atsarginę kopiją ir jį pakeistumėte." acquired = "Sėkmingai įgyta {name}." -already_installed = "Paketas {key} jau įdiegtas, atsisiuntimas praleidžiamas." +"already_installed" = "Paketas {key} jau įdiegtas, atsisiuntimas praleidžiamas. Naudokite overwrite=true, kad sukurtumėte atsarginę kopiją ir jį pakeistumėte." in_progress = "Jau vyksta kitas „Iris“ paketo atsisiuntimas. Palaukite, kol jis bus baigtas, ir bandykite dar kartą." validation_failed = "Pakuotė \"{pack}\"nepavyko patvirtinimas; pasaulio ir Studio kūrimas bus atsisakyta. Motyvai:" validation_reason = " - {reason}" validated_with_warnings = { other = "Pakuotė \"{pack}\"patvirtinta {count} įspėjimai.", one = "Pakuotė \"{pack}\"patvirtinta {count} įspėjimas." } validated = "Pakuotė \"{pack}\"patvirtintas." +"close_studio" = "Prieš pakeisdami įdiegtą paketą, uždarykite aktyvų Studio projektą." +"backup_retained" = "Ankstesnis paketas išsaugotas čia: {path}." + [iris.client.vision] title = "Iris Regėjimas" diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml index a73f4352b..ac4c3d4ac 100644 --- a/core/src/main/resources/languages/nl_NL.toml +++ b/core/src/main/resources/languages/nl_NL.toml @@ -1577,6 +1577,10 @@ failed_fragment = " mislukt {failed}" "commandpack.compat_header" = "§ePack '{pack}': inhoud niet beschikbaar op Minecraft {version}" "commandpack.compat_none" = "§aPack '{pack}' gebruikt geen inhoud die ontbreekt in Minecraft {version}." "commandpack.compat_remedy" = "§7Werk de server bij naar een nieuwere Minecraft om deze inhoud te herstellen, of declareer terugvallen (blockFallbacks van de dimensie, backup van het blok)." +"commandpack.update_world_requires_persistent_world" = "§cKies een geladen Iris-wereld. Studio-werelden kunnen geen generatie-updates ontvangen." +"commandpack.update_world_staged" = "§a{pack} v{version} klaargezet voor wereld '{world}'. Een nette herstart activeert het pakket met terreinovergangen." +"commandpack.update_world_unchanged" = "§aWereld '{world}' gebruikt al {pack} v{version} met dezelfde generatie-instellingen. Herstarten is niet nodig." + [iris.runtime.studio] opening = "§6Studio §bOPENEN" @@ -1688,7 +1692,7 @@ chunk_failed = "Chunk. {x},{z} mislukt: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris-pack '{pack}' geïnstalleerd§8 | §f{transferred}§7 in §f{elapsed}" -"progress.unchanged" = "§eIris-pack '{pack}' is al geïnstalleerd." +"progress.unchanged" = "§eIris-pack '{pack}' is al geïnstalleerd. Gebruik overwrite=true om een reservekopie te maken en het te vervangen." "progress.failed" = "§cDownload van het Iris-pack mislukt.§7 Bekijk de downloadgegevens hierboven en probeer het opnieuw." "progress.failed_detail" = "§cDownload van het Iris-pack mislukt.§7 {error}" "progress.cancelled" = "§eDownload van het Iris-pack geannuleerd vóór publicatie." @@ -1714,14 +1718,17 @@ check_github = "Controleer of het aanwezig is op GitHub En meld dit aan het pers invalid_dimension = "Ongeldige dimensiemap onder dimensions/." importing = "Importeren {name} ({key})" dimension_key_conflict = "Een andere dimensie in de packs-map gebruikt al de sleutel {key}. Importeren mislukt!" -pack_key_conflict = "Een ander pakje gebruikt de sleutel {key}. Importeren mislukt!" +"pack_key_conflict" = "Iris-pack '{key}' is al geïnstalleerd. Gebruik overwrite=true om een reservekopie te maken en het te vervangen." acquired = "Succesvol verworven {name}." -already_installed = "Pack {key} is al geïnstalleerd, download wordt overgeslagen." +"already_installed" = "Pack {key} is al geïnstalleerd, download wordt overgeslagen. Gebruik overwrite=true om een reservekopie te maken en het te vervangen." in_progress = "Er wordt al een ander Iris-pack gedownload. Wacht tot dit is voltooid voordat je het opnieuw probeert." validation_failed = "Verpakking{pack}' mislukte validatie; wereld en Studio creatie zal worden geweigerd. Motivering:" validation_reason = " - {reason}" validated_with_warnings = { other = "Verpakking{pack}' gevalideerd met {count} waarschuwingen.", one = "Verpakking{pack}' gevalideerd met {count} waarschuwing." } validated = "Verpakking{pack}' gevalideerd." +"close_studio" = "Sluit het actieve Studio-project voordat je een geïnstalleerd pakket vervangt." +"backup_retained" = "Vorig pakket opgeslagen in {path}." + [iris.client.vision] title = "Iris Gezicht" diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml index e2f21fccf..c0954a64f 100644 --- a/core/src/main/resources/languages/pl_PL.toml +++ b/core/src/main/resources/languages/pl_PL.toml @@ -1582,6 +1582,10 @@ failed_fragment = " nieudany {failed}" "commandpack.compat_header" = "§ePaczka '{pack}': zawartość niedostępna w Minecraft {version}" "commandpack.compat_none" = "§aPaczka '{pack}' nie używa zawartości brakującej w Minecraft {version}." "commandpack.compat_remedy" = "§7Zaktualizuj serwer do nowszego Minecraft, aby przywrócić tę zawartość, albo zadeklaruj zamienniki (blockFallbacks wymiaru, backup bloku)." +"commandpack.update_world_requires_persistent_world" = "§cWybierz wczytany świat Iris. Światy Studio nie mogą otrzymywać aktualizacji generowania." +"commandpack.update_world_staged" = "§aPrzygotowano {pack} v{version} dla świata '{world}'. Prawidłowy restart aktywuje pakiet z przejściami terenu." +"commandpack.update_world_unchanged" = "§aŚwiat '{world}' już używa {pack} v{version} z tymi samymi ustawieniami generowania. Restart nie jest potrzebny." + [iris.runtime.studio] opening = "§6Studio §bOTWIERANIE" @@ -1693,7 +1697,7 @@ chunk_failed = "Chunk Przewodniczący {x},{z} niepowodzenie: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aPakiet Iris '{pack}' zainstalowany§8 | §f{transferred}§7 w §f{elapsed}" -"progress.unchanged" = "§ePakiet Iris '{pack}' jest już zainstalowany." +"progress.unchanged" = "§ePakiet Iris '{pack}' jest już zainstalowany. Użyj overwrite=true, aby utworzyć kopię zapasową i go zastąpić." "progress.failed" = "§cPobieranie pakietu Iris nie powiodło się.§7 Sprawdź powyższe szczegóły pobierania i spróbuj ponownie." "progress.failed_detail" = "§cPobieranie pakietu Iris nie powiodło się.§7 {error}" "progress.cancelled" = "§ePobieranie pakietu Iris anulowano przed publikacją." @@ -1719,14 +1723,17 @@ check_github = "Sprawdź, czy jest obecny na GitHub i zgłosić do do personelu! invalid_dimension = "Nieprawidłowy folder wymiarów pod dimensions/." importing = "Przywóz {name} ({key})" dimension_key_conflict = "Inny wymiar w folderze pakietów już używa klucza {key}. Import nie powiódł się!" -pack_key_conflict = "Inny pakiet używa klucza {key}. Import nie powiódł się!" +"pack_key_conflict" = "Pakiet Iris '{key}' jest już zainstalowany. Użyj overwrite=true, aby utworzyć kopię zapasową i go zastąpić." acquired = "Udane nabycie {name}." -already_installed = "Pakiet {key} jest już zainstalowany, pomijanie pobierania." +"already_installed" = "Pakiet {key} jest już zainstalowany, pomijanie pobierania. Użyj overwrite=true, aby utworzyć kopię zapasową i go zastąpić." in_progress = "Trwa już pobieranie innego pakietu Iris. Poczekaj na jego zakończenie, zanim spróbujesz ponownie." validation_failed = "Paczka \"{pack}\"nieudaną walidację; świat i tworzenie studia zostaną odrzucone. Uzasadnienie:" validation_reason = " - {reason}" validated_with_warnings = { other = "Paczka \"{pack}\"zatwierdzone {count} Ostrzeżenia.", one = "Paczka \"{pack}\"zatwierdzone {count} Ostrzeżenie." } validated = "Paczka \"{pack}\"zatwierdzone." +"close_studio" = "Zamknij aktywny projekt Studio przed zastąpieniem zainstalowanego pakietu." +"backup_retained" = "Poprzedni pakiet zapisano w {path}." + [iris.client.vision] title = "Iris Wizja" diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml index 522647681..d5670c83d 100644 --- a/core/src/main/resources/languages/pt_PT.toml +++ b/core/src/main/resources/languages/pt_PT.toml @@ -1577,6 +1577,10 @@ failed_fragment = " falhou {failed}" "commandpack.compat_header" = "§ePack '{pack}': conteúdo indisponível no Minecraft {version}" "commandpack.compat_none" = "§aO pack '{pack}' não usa conteúdo em falta no Minecraft {version}." "commandpack.compat_remedy" = "§7Atualize o servidor para um Minecraft mais recente para restaurar este conteúdo, ou declare alternativas (blockFallbacks da dimensão, backup do bloco)." +"commandpack.update_world_requires_persistent_world" = "§cEscolhe um mundo Iris carregado. Os mundos Studio não podem receber atualizações de geração." +"commandpack.update_world_staged" = "§a{pack} v{version} preparado para o mundo '{world}'. Um reinício normal ativará o pacote com transição do terreno." +"commandpack.update_world_unchanged" = "§aO mundo '{world}' já utiliza {pack} v{version} com as mesmas definições de geração. Não é necessário reiniciar." + [iris.runtime.studio] opening = "§6Studio §bA ABRIR" @@ -1688,7 +1692,7 @@ chunk_failed = "Chunk. {x},{z} falhou: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aPack Iris '{pack}' instalado§8 | §f{transferred}§7 em §f{elapsed}" -"progress.unchanged" = "§eO pack Iris '{pack}' já está instalado." +"progress.unchanged" = "§eO pack Iris '{pack}' já está instalado. Usa overwrite=true para criar uma cópia de segurança e substituí-lo." "progress.failed" = "§cA transferência do pack Iris falhou.§7 Reveja os detalhes da transferência acima e tente novamente." "progress.failed_detail" = "§cA transferência do pack Iris falhou.§7 {error}" "progress.cancelled" = "§eA transferência do pack Iris foi cancelada antes da publicação." @@ -1714,14 +1718,17 @@ check_github = "Verifique se está presente em GitHub E reporta isto ao pessoal! invalid_dimension = "Pasta de dimensões inválida sob dimensions/." importing = "Importação {name} ({key})" dimension_key_conflict = "Outra dimensão na pasta pacotes já está usando a chave {key}. A importação falhou!" -pack_key_conflict = "Outro pacote está usando a chave {key}. A importação falhou!" +"pack_key_conflict" = "O pack Iris '{key}' já está instalado. Usa overwrite=true para criar uma cópia de segurança e substituí-lo." acquired = "Adquirido com sucesso {name}." -already_installed = "O pack {key} já está instalado, download ignorado." +"already_installed" = "O pack {key} já está instalado, download ignorado. Usa overwrite=true para criar uma cópia de segurança e substituí-lo." in_progress = "Já está em curso a transferência de outro pack Iris. Aguarde que termine antes de tentar novamente." validation_failed = "Embalar '{pack}' validação falhada; a criação de mundo e estúdio será recusada. Motivos:" validation_reason = " - {reason}" validated_with_warnings = { other = "Embalar '{pack}' validado com {count} avisos.", one = "Embalar '{pack}' validado com {count} Aviso." } validated = "Embalar '{pack}' validado." +"close_studio" = "Fecha o projeto Studio ativo antes de substituir um pacote instalado." +"backup_retained" = "Pacote anterior guardado em {path}." + [iris.client.vision] title = "Iris Visão" diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml index 3e6afffc8..9f9214829 100644 --- a/core/src/main/resources/languages/ru_RU.toml +++ b/core/src/main/resources/languages/ru_RU.toml @@ -1582,6 +1582,10 @@ failed_fragment = " неудачный {failed}" "commandpack.compat_header" = "§eПак '{pack}': содержимое недоступно в Minecraft {version}" "commandpack.compat_none" = "§aПак '{pack}' не использует содержимое, которого нет в Minecraft {version}." "commandpack.compat_remedy" = "§7Обновите сервер до более новой версии Minecraft, чтобы вернуть это содержимое, или объявите замены (blockFallbacks измерения, backup блока)." +"commandpack.update_world_requires_persistent_world" = "§cВыберите загруженный мир Iris. Миры Studio не могут получать обновления генерации." +"commandpack.update_world_staged" = "§a{pack} v{version} подготовлен для мира '{world}'. Штатный перезапуск активирует пакет с плавным переходом рельефа." +"commandpack.update_world_unchanged" = "§aМир '{world}' уже использует {pack} v{version} с теми же настройками генерации. Перезапуск не требуется." + [iris.runtime.studio] opening = "§6Studio §bОТКРЫТИЕ" @@ -1693,7 +1697,7 @@ chunk_failed = "Кусок {x},{z} не удалось: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aПак Iris «{pack}» установлен§8 | §f{transferred}§7 за §f{elapsed}" -"progress.unchanged" = "§eПак Iris «{pack}» уже установлен." +"progress.unchanged" = "§eПак Iris «{pack}» уже установлен. Используйте overwrite=true, чтобы создать резервную копию и заменить его." "progress.failed" = "§cНе удалось загрузить пак Iris.§7 Проверьте сведения о загрузке выше и повторите попытку." "progress.failed_detail" = "§cНе удалось загрузить пак Iris.§7 {error}" "progress.cancelled" = "§eЗагрузка пака Iris отменена до публикации." @@ -1719,14 +1723,17 @@ check_github = "Убедитесь, что он присутствует на Gi invalid_dimension = "Недействительная размерная папка под dimensions/." importing = "импортировать {name} ({key})" dimension_key_conflict = "Еще одно измерение в папке пакетов уже использует ключ {key}. Импорт провалился!" -pack_key_conflict = "Другой пакет использует ключ. {key}. Импорт провалился!" +"pack_key_conflict" = "Пак Iris «{key}» уже установлен. Используйте overwrite=true, чтобы создать резервную копию и заменить его." acquired = "Успешно приобретенный {name}." -already_installed = "Пак {key} уже установлен, загрузка пропущена." +"already_installed" = "Пак {key} уже установлен, загрузка пропущена. Используйте overwrite=true, чтобы создать резервную копию и заменить его." in_progress = "Уже выполняется загрузка другого пака Iris. Дождитесь её завершения, прежде чем повторить попытку." validation_failed = "Пакуй.{pack}Неудачная проверка; мир и создание студии будут отклонены. Причины:" validation_reason = " - {reason}" validated_with_warnings = { other = "Пакуй.{pack}подтвержденный {count} предупреждения.", one = "Пакуй.{pack}подтвержденный {count} предупреждение." } validated = "Пакуй.{pack}Подтверждено." +"close_studio" = "Закройте активный проект Studio перед заменой установленного пакета." +"backup_retained" = "Предыдущий пакет сохранён в {path}." + [iris.client.vision] title = "Iris Видение" diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml index 6c8aca0ee..da368bce2 100644 --- a/core/src/main/resources/languages/tr_TR.toml +++ b/core/src/main/resources/languages/tr_TR.toml @@ -1582,6 +1582,10 @@ failed_fragment = " Başarısız oldu {failed}" "commandpack.compat_header" = "§ePaket '{pack}': içerik Minecraft {version} sürümünde yok" "commandpack.compat_none" = "§aPaket '{pack}' Minecraft {version} sürümünde eksik olan içerik kullanmıyor." "commandpack.compat_remedy" = "§7Bu içeriği geri kazanmak için sunucuyu daha yeni bir Minecraft sürümüne güncelleyin veya yedekler tanımlayın (boyutun blockFallbacks, bloğun backup)." +"commandpack.update_world_requires_persistent_world" = "§cYüklü bir Iris dünyası seçin. Studio dünyaları üretim güncellemeleri alamaz." +"commandpack.update_world_staged" = "§a'{world}' dünyası için {pack} v{version} hazırlandı. Düzgün bir yeniden başlatma, paketi arazi geçişleriyle etkinleştirir." +"commandpack.update_world_unchanged" = "§a'{world}' dünyası aynı üretim ayarlarıyla zaten {pack} v{version} kullanıyor. Yeniden başlatma gerekmiyor." + [iris.runtime.studio] opening = "§6Studio §bAÇILIYOR" @@ -1693,7 +1697,7 @@ chunk_failed = "Chunk. {x},{z} başarısız: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris paketi '{pack}' kuruldu§8 | §f{transferred}§7, süre: §f{elapsed}" -"progress.unchanged" = "§eIris paketi '{pack}' zaten kurulu." +"progress.unchanged" = "§eIris paketi '{pack}' zaten kurulu. Yedeklemek ve değiştirmek için overwrite=true kullanın." "progress.failed" = "§cIris paketi indirilemedi.§7 Yukarıdaki indirme ayrıntılarını inceleyip yeniden deneyin." "progress.failed_detail" = "§cIris paketi indirilemedi.§7 {error}" "progress.cancelled" = "§eIris paketi indirme işlemi yayınlanmadan önce iptal edildi." @@ -1719,14 +1723,17 @@ check_github = "GitHub'de mevcut olduğunu kontrol edin ve bunu personele rapor invalid_dimension = "geçersiz boyut klasörü altında dimensions/." importing = "İthalat {name} ({key})" dimension_key_conflict = "Paketlerdeki başka bir boyut zaten anahtarı kullanıyor {key}. İthalat başarısız oldu!" -pack_key_conflict = "Başka bir paket anahtarı kullanıyor {key}. İthalat başarısız oldu!" +"pack_key_conflict" = "Iris paketi '{key}' zaten kurulu. Yedeklemek ve değiştirmek için overwrite=true kullanın." acquired = "Başarılı bir şekilde satın alındı {name}." -already_installed = "{key} paketi zaten kurulu, indirme atlanıyor." +"already_installed" = "{key} paketi zaten kurulu, indirme atlanıyor. Yedeklemek ve değiştirmek için overwrite=true kullanın." in_progress = "Başka bir Iris paketi indirme işlemi zaten devam ediyor. Yeniden denemeden önce tamamlanmasını bekleyin." validation_failed = "Pack \"{pack}“Başarısız doğrulama; dünya ve Stüdyo yaratımı reddedilecektir. Sebepler:" validation_reason = " - {reason}" validated_with_warnings = { other = "Pack \"{pack}\" ile doğrulandı\" {count} Uyarılar.", one = "Pack \"{pack}\" ile doğrulandı\" {count} Uyarı." } validated = "Pack \"{pack}\"Doğrulandı." +"close_studio" = "Yüklü bir paketi değiştirmeden önce etkin Studio projesini kapatın." +"backup_retained" = "Önceki paket {path} konumuna kaydedildi." + [iris.client.vision] title = "Iris Vizyon" diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml index 9afdd189e..17310c5f6 100644 --- a/core/src/main/resources/languages/vi_VI.toml +++ b/core/src/main/resources/languages/vi_VI.toml @@ -1580,6 +1580,10 @@ failed_fragment = " bị lỗi {failed}" "commandpack.compat_header" = "§eGói '{pack}': nội dung không có trên Minecraft {version}" "commandpack.compat_none" = "§aGói '{pack}' không dùng nội dung nào thiếu trên Minecraft {version}." "commandpack.compat_remedy" = "§7Cập nhật máy chủ lên Minecraft mới hơn để khôi phục nội dung này, hoặc khai báo phương án dự phòng (blockFallbacks của chiều, backup của khối)." +"commandpack.update_world_requires_persistent_world" = "§cChọn một thế giới Iris đã được tải. Thế giới Studio không thể nhận bản cập nhật tạo địa hình." +"commandpack.update_world_staged" = "§aĐã chuẩn bị {pack} v{version} cho thế giới '{world}'. Khởi động lại đúng cách sẽ kích hoạt gói với chuyển tiếp địa hình." +"commandpack.update_world_unchanged" = "§aThế giới '{world}' đã dùng {pack} v{version} với cùng thiết lập tạo địa hình. Không cần khởi động lại." + [iris.runtime.studio] opening = "§6Studio §bĐANG MỞ" @@ -1691,7 +1695,7 @@ chunk_failed = "Chunk. {x},{z} thất bại: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aĐã cài đặt gói Iris '{pack}'§8 | §f{transferred}§7 trong §f{elapsed}" -"progress.unchanged" = "§eGói Iris '{pack}' đã được cài đặt." +"progress.unchanged" = "§eGói Iris '{pack}' đã được cài đặt. Dùng overwrite=true để sao lưu và thay thế gói." "progress.failed" = "§cKhông thể tải gói Iris.§7 Hãy xem lại thông tin tải xuống ở trên rồi thử lại." "progress.failed_detail" = "§cKhông thể tải gói Iris.§7 {error}" "progress.cancelled" = "§eĐã hủy tải gói Iris trước khi xuất bản." @@ -1717,14 +1721,17 @@ check_github = "Kiểm tra xem nó có ở trên không GitHub và báo cáo v invalid_dimension = "Thư mục chiều không hợp lệ nằm dưới dimensions/." importing = "Nhập {name} ({key})" dimension_key_conflict = "Một chiều không gian khác trong thư mục gói là đã sử dụng phím {key}. Nhập thất bại!" -pack_key_conflict = "Name {key}. Nhập thất bại!" +"pack_key_conflict" = "Gói Iris '{key}' đã được cài đặt. Dùng overwrite=true để sao lưu và thay thế gói." acquired = "Được thành công {name}." -already_installed = "Gói {key} đã được cài đặt, bỏ qua tải xuống." +"already_installed" = "Gói {key} đã được cài đặt, bỏ qua tải xuống. Dùng overwrite=true để sao lưu và thay thế gói." in_progress = "Một gói Iris khác đang được tải xuống. Hãy chờ quá trình này hoàn tất trước khi thử lại." validation_failed = "Gói '{pack}'Đã thất bại trong việc xác nhận; thế giới và phòng thu sẽ bị từ chối. Lý do:" validation_reason = " - {reason}" validated_with_warnings = { other = "Gói '{pack}Xác nhận với {count} Cảnh báo.", one = "Gói '{pack}Xác nhận với {count} Cảnh báo." } validated = "Gói '{pack}Xác nhận." +"close_studio" = "Đóng dự án Studio đang hoạt động trước khi thay thế gói đã cài đặt." +"backup_retained" = "Đã lưu gói trước đó tại {path}." + [iris.client.vision] title = "Iris Tầm nhìn" diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml index 27ea089df..22d81e30b 100644 --- a/core/src/main/resources/languages/zh_CN.toml +++ b/core/src/main/resources/languages/zh_CN.toml @@ -1580,6 +1580,10 @@ failed_fragment = " 失败 {failed}" "commandpack.compat_header" = "§e资源包 '{pack}': 内容在 Minecraft {version} 中不可用" "commandpack.compat_none" = "§a资源包 '{pack}' 未使用 Minecraft {version} 中缺失的内容。" "commandpack.compat_remedy" = "§7将服务器升级到更新的 Minecraft 以恢复这些内容,或声明回退 (维度的 blockFallbacks、方块的 backup)。" +"commandpack.update_world_requires_persistent_world" = "§c请选择已加载的 Iris 世界。Studio 世界无法接收生成更新。" +"commandpack.update_world_staged" = "§a已为世界 '{world}' 暂存 {pack} v{version}。正常重启后将启用该包,并进行地形融合。" +"commandpack.update_world_unchanged" = "§a世界 '{world}' 已使用 {pack} v{version},且生成设置相同。无需重启。" + [iris.runtime.studio] opening = "§6工作室 §b开幕" @@ -1691,7 +1695,7 @@ chunk_failed = "块 {x},{z} 失败: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris 包“{pack}”已安装§8 | §f{transferred}§7,用时 §f{elapsed}" -"progress.unchanged" = "§eIris 包“{pack}”已安装。" +"progress.unchanged" = "§eIris 包“{pack}”已安装。 使用 overwrite=true 备份并替换该包。" "progress.failed" = "§cIris 包下载失败。§7 请查看上方的下载详情后重试。" "progress.failed_detail" = "§cIris 包下载失败。§7 {error}" "progress.cancelled" = "§eIris 包下载已在发布前取消。" @@ -1717,14 +1721,17 @@ check_github = "检查它是否在 GitHub 把这个报告给工作人员!" invalid_dimension = "无效的维度文件夹 dimensions/." importing = "导入 {name} ({key})" dimension_key_conflict = "包文件夹中的另一个维度已经使用密钥 {key}. 导入失败 !" -pack_key_conflict = "另一个包是用钥匙 {key}. 导入失败 !" +"pack_key_conflict" = "Iris 包“{key}”已安装。 使用 overwrite=true 备份并替换该包。" acquired = "已成功获取 {name}." -already_installed = "包 {key} 已安装,跳过下载。" +"already_installed" = "包 {key} 已安装,跳过下载。 使用 overwrite=true 备份并替换该包。" in_progress = "另一个 Iris 包下载已在进行中。请等待其完成后再试。" validation_failed = "包{pack}' 验证失败; 世界和工作室的创建将被拒绝. 原因:" validation_reason = " - {reason}" validated_with_warnings = { other = "包{pack}'经验证 {count} 警告! 警告你们!", one = "包{pack}'经验证 {count} 警告,你应该知道" } validated = "包{pack}已确认。" +"close_studio" = "替换已安装的包之前,请关闭当前 Studio 项目。" +"backup_retained" = "旧包已保存到 {path}。" + [iris.client.vision] title = "Iris 愿景" diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml index 38bffe2d7..fc1ebeafe 100644 --- a/core/src/main/resources/languages/zh_TW.toml +++ b/core/src/main/resources/languages/zh_TW.toml @@ -1580,6 +1580,10 @@ failed_fragment = " 失敗 {failed}" "commandpack.compat_header" = "§e資源包 '{pack}': 內容在 Minecraft {version} 中無法使用" "commandpack.compat_none" = "§a資源包 '{pack}' 未使用 Minecraft {version} 中缺少的內容。" "commandpack.compat_remedy" = "§7將伺服器升級到較新的 Minecraft 以還原這些內容,或宣告備援 (維度的 blockFallbacks、方塊的 backup)。" +"commandpack.update_world_requires_persistent_world" = "§c請選擇已載入的 Iris 世界。Studio 世界無法接收生成更新。" +"commandpack.update_world_staged" = "§a已為世界 '{world}' 暫存 {pack} v{version}。正常重新啟動後將啟用此套件,並進行地形融合。" +"commandpack.update_world_unchanged" = "§a世界 '{world}' 已使用 {pack} v{version},且生成設定相同。無需重新啟動。" + [iris.runtime.studio] opening = "§6工作室 §b開幕" @@ -1691,7 +1695,7 @@ chunk_failed = "塊 {x},{z} 失敗: {type}" "progress.indeterminate" = "{bar}§7 §b{phase}§8 | §f{transferred}§8 | §b{rate}/s" "progress.detail" = "§8 - §7{detail}" "progress.complete" = "§aIris 套件「{pack}」已安裝§8 | §f{transferred}§7,用時 §f{elapsed}" -"progress.unchanged" = "§eIris 套件「{pack}」已安裝。" +"progress.unchanged" = "§eIris 套件「{pack}」已安裝。 使用 overwrite=true 備份並替換此套件。" "progress.failed" = "§cIris 套件下載失敗。§7 請檢視上方的下載詳細資訊後重試。" "progress.failed_detail" = "§cIris 套件下載失敗。§7 {error}" "progress.cancelled" = "§eIris 套件下載已在發佈前取消。" @@ -1717,14 +1721,17 @@ check_github = "檢查它是否在 GitHub 把這個報告給工作人員!" invalid_dimension = "無效的維度資料夾 dimensions/." importing = "匯入 {name} ({key})" dimension_key_conflict = "包資料夾中的另一個維度已經使用金鑰 {key}. 匯入失敗 !" -pack_key_conflict = "另一個包是用鑰匙 {key}. 匯入失敗 !" +"pack_key_conflict" = "Iris 套件「{key}」已安裝。 使用 overwrite=true 備份並替換此套件。" acquired = "已成功獲取 {name}." -already_installed = "套件 {key} 已安裝,跳過下載。" +"already_installed" = "套件 {key} 已安裝,跳過下載。 使用 overwrite=true 備份並替換此套件。" in_progress = "另一個 Iris 套件下載已在進行中。請等待其完成後再試。" validation_failed = "包{pack}' 驗證失敗; 世界和工作室的建立將被拒絕. 原因:" validation_reason = " - {reason}" validated_with_warnings = { other = "包{pack}'經驗證 {count} 警告! 警告你們!", one = "包{pack}'經驗證 {count} 警告,你應該知道" } validated = "包{pack}已確認。" +"close_studio" = "替換已安裝的套件之前,請關閉目前的 Studio 專案。" +"backup_retained" = "舊套件已儲存至 {path}。" + [iris.client.vision] title = "Iris 願景" diff --git a/core/src/test/java/art/arcane/iris/diagnostics/splash/IrisSplashComposerTest.java b/core/src/test/java/art/arcane/iris/diagnostics/splash/IrisSplashComposerTest.java index 2654112d9..1e2403373 100644 --- a/core/src/test/java/art/arcane/iris/diagnostics/splash/IrisSplashComposerTest.java +++ b/core/src/test/java/art/arcane/iris/diagnostics/splash/IrisSplashComposerTest.java @@ -1,11 +1,28 @@ package art.arcane.iris.diagnostics.splash; +import art.arcane.iris.pack.BuiltInPackUpdates; import org.junit.Test; +import java.util.List; +import java.util.Map; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; public class IrisSplashComposerTest { + @Test + public void packLinesShowUpdatesAndKeepCustomPacks() { + List packs = List.of( + new IrisSplashPackScanner.SplashPackMetadata("custom", "7"), + new IrisSplashPackScanner.SplashPackMetadata("overworld", "4009"), + new IrisSplashPackScanner.SplashPackMetadata("underworld", "1012")); + assertEquals(List.of("Custom Dimensions: 3", " custom v7", " overworld v4009 -> v4010 available", + " underworld v1012", "Update overworld: /iris download pack=overworld overwrite=true"), + IrisSplashComposer.composePackLines(packs, Map.of( + "overworld", new BuiltInPackUpdates.Update("4010"), + "underworld", new BuiltInPackUpdates.Update("1012")))); + } + @Test public void composeInfoUsesCurrentVersionWithoutStaleReleaseTag() { String[] info = IrisSplashComposer.composeInfo("4.0.0-26.2", "Paper 26.2", IrisSplashComposer.InfoStyle.PLAIN); diff --git a/core/src/test/java/art/arcane/iris/pack/AtomicDirectoryPublisherTest.java b/core/src/test/java/art/arcane/iris/pack/AtomicDirectoryPublisherTest.java index 5e6be74fe..5395af1c0 100644 --- a/core/src/test/java/art/arcane/iris/pack/AtomicDirectoryPublisherTest.java +++ b/core/src/test/java/art/arcane/iris/pack/AtomicDirectoryPublisherTest.java @@ -18,6 +18,31 @@ public class AtomicDirectoryPublisherTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Test + public void retainedBackupSurvivesCommitAndRemainsAvailableForRollback() throws Exception { + Path root = temporaryFolder.getRoot().toPath(); + Path target = Files.createDirectory(root.resolve("target")); + Files.writeString(target.resolve("value.txt"), "old"); + Path staged = Files.createDirectory(root.resolve("stage")); + Files.writeString(staged.resolve("value.txt"), "new"); + Path retained = root.resolve("retained"); + + try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staged, target)) { + assertTrue(publication.retainBackup(retained)); + } + + assertEquals("old", Files.readString(target.resolve("value.txt"))); + assertFalse(Files.exists(retained)); + Files.createDirectory(staged); + Files.writeString(staged.resolve("value.txt"), "new"); + try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staged, target)) { + assertTrue(publication.retainBackup(retained)); + publication.commit(); + } + assertEquals("old", Files.readString(retained.resolve("value.txt"))); + assertEquals("new", Files.readString(target.resolve("value.txt"))); + } + @Test public void commitPublishesStagedDirectoryAndRemovesBackup() throws Exception { Path root = temporaryFolder.getRoot().toPath(); diff --git a/core/src/test/java/art/arcane/iris/pack/BuiltInPackUpdatesTest.java b/core/src/test/java/art/arcane/iris/pack/BuiltInPackUpdatesTest.java new file mode 100644 index 000000000..ecbf628b2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/pack/BuiltInPackUpdatesTest.java @@ -0,0 +1,129 @@ +package art.arcane.iris.pack; + +import com.sun.net.httpserver.HttpServer; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class BuiltInPackUpdatesTest { + @Test + public void comparesNumericVersionsWithoutDowngradeNotices() { + BuiltInPackUpdates.Update update = new BuiltInPackUpdates.Update("4010"); + assertEquals(" -> v4010 available", update.suffix("999")); + assertEquals("", update.suffix("4010")); + assertEquals("", update.suffix("5000")); + assertEquals(" (latest v4010)", update.suffix("custom")); + assertFalse(update.newerThan("custom")); + assertEquals(" (update check unavailable)", BuiltInPackUpdates.Update.unavailable().suffix("4010")); + } + + @Test + public void checksOnlyBuiltInPacksAndKeepsSuccessfulResultsOnPartialFailure() throws Exception { + AtomicInteger requests = new AtomicInteger(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + requests.incrementAndGet(); + boolean overworld = exchange.getRequestURI().getPath().equals("/overworld"); + byte[] body = (overworld ? release("v4010", "overworld.zip", false) : "rate limited") + .getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(overworld ? 200 : 429, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try (HttpClient client = HttpClient.newHttpClient()) { + Map updates = BuiltInPackUpdates.check( + List.of("overworld", "underworld", "custom", "overworld"), client, + pack -> URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/" + pack)) + .get(10, TimeUnit.SECONDS); + assertEquals(2, requests.get()); + assertEquals("4010", updates.get("overworld").version()); + assertEquals(BuiltInPackUpdates.Update.unavailable(), updates.get("underworld")); + assertFalse(updates.containsKey("custom")); + } finally { + server.stop(0); + } + } + + @Test + public void rejectsMalformedPrereleaseAndMissingArchiveMetadata() throws Exception { + for (String body : List.of("invalid", "{}", release("4010", "source.zip", false), + release("4010", "overworld.zip", true), release("not-a-version", "overworld.zip", false))) { + assertEquals(BuiltInPackUpdates.Update.unavailable(), checkResponse(body)); + } + } + + @Test + public void acceptsStableReleaseWithMatchingArchive() throws Exception { + assertTrue(checkResponse(release("V+4010", "overworld.zip", false)).newerThan("4009")); + } + + @Test + public void boundsReleaseMetadataSize() throws Exception { + assertEquals(BuiltInPackUpdates.Update.unavailable(), checkResponse(" ".repeat(1024 * 1024 + 1))); + } + + @Test + public void timesOutWithoutResponseHeaders() throws Exception { + assertStalledResponseUnavailable(false); + } + + @Test + public void timesOutWhenResponseBodyStalls() throws Exception { + assertStalledResponseUnavailable(true); + } + + private void assertStalledResponseUnavailable(boolean sendHeaders) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + if (sendHeaders) { + exchange.sendResponseHeaders(200, 100); + exchange.getResponseBody().write('{'); + exchange.getResponseBody().flush(); + } + }); + server.start(); + try (HttpClient client = HttpClient.newHttpClient()) { + assertEquals(BuiltInPackUpdates.Update.unavailable(), BuiltInPackUpdates.check( + List.of("overworld"), client, + pack -> URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/")) + .get(8, TimeUnit.SECONDS).get("overworld")); + } finally { + server.stop(0); + } + } + + private BuiltInPackUpdates.Update checkResponse(String response) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + byte[] body = response.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try (HttpClient client = HttpClient.newHttpClient()) { + return BuiltInPackUpdates.check(List.of("overworld"), client, + pack -> URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/")) + .get(10, TimeUnit.SECONDS).get("overworld"); + } finally { + server.stop(0); + } + } + + private static String release(String version, String asset, boolean prerelease) { + return "{\"tag_name\":\"" + version + "\",\"draft\":false,\"prerelease\":" + prerelease + + ",\"assets\":[{\"name\":\"" + asset + "\"}]}"; + } +} diff --git a/core/src/test/java/art/arcane/iris/pack/PackDownloaderTest.java b/core/src/test/java/art/arcane/iris/pack/PackDownloaderTest.java index b6aabcd6e..d9c48a162 100644 --- a/core/src/test/java/art/arcane/iris/pack/PackDownloaderTest.java +++ b/core/src/test/java/art/arcane/iris/pack/PackDownloaderTest.java @@ -606,6 +606,13 @@ public class PackDownloaderTest { assertTrue(result.restartRequired()); assertEquals("new", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8)); assertFalse(Files.exists(target.toPath().resolve("old-only.txt"))); + File[] backups = new File(packsFolder, ".backups").listFiles(); + assertNotNull(backups); + assertEquals(1, backups.length); + assertEquals("old", Files.readString(backups[0].toPath().resolve("state.txt"))); + assertEquals("old", Files.readString(backups[0].toPath().resolve("old-only.txt"))); + assertTrue(feedback.stream().anyMatch(message -> message.contains(backups[0].getPath()))); + assertFalse(PackDirectoryResolver.isVisiblePackDirectory(new File(packsFolder, ".backups"))); assertTransactionStateClean(packsFolder); assertEquals(0, PackDownloader.downloadLockCount()); } diff --git a/core/src/test/java/art/arcane/iris/studio/StudioSVCWorldPackPublishTest.java b/core/src/test/java/art/arcane/iris/studio/StudioSVCWorldPackPublishTest.java index 5903883e7..b6920015a 100644 --- a/core/src/test/java/art/arcane/iris/studio/StudioSVCWorldPackPublishTest.java +++ b/core/src/test/java/art/arcane/iris/studio/StudioSVCWorldPackPublishTest.java @@ -7,7 +7,12 @@ import art.arcane.iris.pack.BrokenPackException; import art.arcane.iris.pack.PackValidationRegistry; import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.generation.runtime.PreservationRegistry; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.platform.bukkit.plugin.VolmitSender; +import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.world.history.GenerationHistory; +import art.arcane.iris.world.task.J; import art.arcane.iris.testsupport.PlatformLeakGuard; import org.junit.Assume; import org.junit.After; @@ -35,6 +40,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; public class StudioSVCWorldPackPublishTest { @ClassRule @@ -54,6 +60,34 @@ public class StudioSVCWorldPackPublishTest { IrisServices.remove(PreservationRegistry.class); } + @Test + public void brokenSourceIsRejectedBeforeOpeningOrChangingWorldHistory() throws Exception { + Path source = temporaryFolder.newFolder("invalid-generation-update").toPath(); + writeValidPack(source); + Files.writeString(source.resolve("biomes/biome.json"), "{"); + Path world = temporaryFolder.newFolder("untouched-generation-world").toPath(); + Files.writeString(world.resolve("level.dat"), "world-state"); + IrisData sourceData = mock(IrisData.class); + when(sourceData.getDataFolder()).thenReturn(source.toFile()); + IrisDimension dimension = new IrisDimension(); + dimension.setLoadKey("main"); + dimension.setLoader(sourceData); + VolmitSender sender = mock(VolmitSender.class); + + try (MockedStatic scheduler = mockStatic(J.class); + MockedStatic logging = mockStatic(IrisLogging.class); + MockedStatic history = mockStatic(GenerationHistory.class); + MockedStatic configurator = mockStatic(ServerConfigurator.class)) { + assertNull(new StudioSVC().replaceIntoWorld(sender, dimension, world.toFile(), 42L)); + + history.verifyNoInteractions(); + configurator.verifyNoInteractions(); + assertFalse(Files.exists(world.resolve("iris"))); + assertEquals("world-state", Files.readString(world.resolve("level.dat"))); + assertTrue(PackValidationRegistry.isBroken(source)); + } + } + @Test public void copiesToStageAndPublishesTheCompletePack() throws IOException { Path root = temporaryFolder.newFolder("world").toPath();