diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java index a6bcff211..7d1cdab1a 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java @@ -60,6 +60,7 @@ import java.util.function.Supplier; */ public final class IrisWorldGeneratorResolver { private static final int VALIDATION_STABILITY_ATTEMPTS = 2; + private static final int COMPAT_BOOT_KEY_CAP = 3; private static final Object SNAPSHOT_VALIDATION_LOCK = new Object(); private static final String IRIS_DIMENSION_NAMESPACE = "iris"; private static final String PLOT_SQUARED_DISCOVERY_WORLD = "CheckingPlotSquaredGenerator"; @@ -123,6 +124,9 @@ public final class IrisWorldGeneratorResolver { if (packDirectory != null && packFingerprint != null && !packFingerprint.isBlank()) { PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint); } + String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null; + String compatSummary = PackValidator.compatSummary(result, minecraftVersion); + String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary; if (!result.isLoadable()) { Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world and Studio creation with this pack will be refused. Reasons:"); @@ -131,12 +135,22 @@ public final class IrisWorldGeneratorResolver { } } else if (!result.getWarnings().isEmpty()) { Iris.info("Pack '" + result.getPackName() + "' validated (" - + result.getWarnings().size() + " warning(s))."); + + result.getWarnings().size() + " warning(s))." + compatSuffix); for (String warning : result.getWarnings()) { Iris.warn(" [" + result.getPackName() + "] " + warning); } } else if (cached.isEmpty()) { - Iris.success("Pack '" + result.getPackName() + "' validated."); + if (compatSummary.isEmpty()) { + Iris.success("Pack '" + result.getPackName() + "' validated."); + } else { + Iris.info("Pack '" + result.getPackName() + "' validated." + compatSuffix); + } + } else if (!compatSummary.isEmpty()) { + Iris.info("Pack '" + result.getPackName() + "' validated." + compatSuffix); + } + // Cache hits carry the persisted findings, so the listing prints on every boot, not only after a miss. + for (String line : PackValidator.compatBootLines(result, minecraftVersion, COMPAT_BOOT_KEY_CAP)) { + Iris.warn(line); } } IrisStartupValidation.markPacksReady(); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java index d1ba14c18..3eb1348c0 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java @@ -19,6 +19,7 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; +import art.arcane.iris.core.compat.PackCompatReport; import art.arcane.iris.core.pack.PackDirectoryResolver; import art.arcane.iris.core.pack.PackResourceCleanup; import art.arcane.iris.core.pack.PackValidationCache; @@ -34,10 +35,13 @@ import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.volmlib.util.localization.TextKey; +import art.arcane.iris.spi.IrisPlatforms; + import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.BukkitCommandMessages; @@ -250,6 +254,76 @@ public class CommandPack implements DirectorExecutor { reportResult(s, result); } + @Director(description = "Show pack content that cannot generate on this Minecraft version", descriptionKey = "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version", aliases = {"cp"}) + public void compat( + @Param(description = "The pack folder name to report on, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_compat", defaultValue = "*") + String pack + ) { + VolmitSender s = sender(); + if (wantsAllPacks(pack)) { + Map snapshot = PackValidationRegistry.snapshot(); + if (snapshot.isEmpty()) { + s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST)); + return; + } + for (Map.Entry entry : snapshot.entrySet()) { + for (String line : compatOutput(entry.getKey(), entry.getValue())) { + s.sendMessage(line); + } + } + return; + } + for (String line : compatOutput(pack, PackValidationRegistry.get(pack))) { + s.sendMessage(line); + } + } + + /** + * The compat listing for one pack, straight from the persisted validation so no pack is reloaded. Every key is + * printed with every subject; the boot listing is the capped form of the same data. + */ + static List compatOutput(String pack, PackValidationResult result) { + List lines = new ArrayList<>(); + if (result == null) { + lines.add(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, + MessageArgument.untrusted("pack", String.valueOf(pack)), + MessageArgument.untrusted("pack2", String.valueOf(pack)))); + return lines; + } + String version = compatVersion(result); + if (result.getCompatFindings().isEmpty()) { + lines.add(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_COMPAT_NONE, + MessageArgument.untrusted("pack", pack), + MessageArgument.untrusted("version", version))); + return lines; + } + lines.add(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_COMPAT_HEADER, + MessageArgument.untrusted("pack", pack), + MessageArgument.untrusted("version", version))); + for (String line : PackCompatReport.of(result.getCompatFindings()).keyLines(0)) { + lines.add(IrisLanguage.text( + BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2, + MessageArgument.untrusted("value", line))); + } + lines.add(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_COMPAT_REMEDY)); + return lines; + } + + /** The live Minecraft version, falling back to the one the validation ran against. */ + static String compatVersion(PackValidationResult result) { + if (IrisPlatforms.isBound()) { + String live = IrisPlatforms.get().minecraftVersion(); + if (live != null && !live.isBlank()) { + return live; + } + } + String persisted = result.getMinecraftVersion(); + return persisted == null || persisted.isBlank() ? "unknown" : persisted; + } + private PackValidationResult runValidate(VolmitSender s, File packFolder) { try { PackValidationResult result = PackValidator.validate(packFolder); diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPackCompatTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPackCompatTest.java new file mode 100644 index 000000000..c85d1ed66 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandPackCompatTest.java @@ -0,0 +1,94 @@ +package art.arcane.iris.core.commands; + +import art.arcane.iris.core.compat.CompatAction; +import art.arcane.iris.core.compat.CompatFinding; +import art.arcane.iris.core.compat.CompatRegistry; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.volmlib.util.director.compat.DirectorAnnotationCompatibility; +import art.arcane.volmlib.util.director.runtime.DirectorNodeDescriptor; +import art.arcane.volmlib.util.director.runtime.DirectorParameterDescriptor; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CommandPackCompatTest { + private IrisPlatform previousPlatform; + + /** The persisted Minecraft version is only used when no platform is bound, so pin that here. */ + @Before + public void unbindPlatform() { + previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.unbind(); + } + + @After + public void rebindPlatform() { + IrisPlatforms.unbind(); + if (previousPlatform != null) { + IrisPlatforms.bind(previousPlatform); + } + } + + private static CompatFinding block(CompatAction action, String subjectKey) { + return new CompatFinding( + CompatRegistry.BLOCK, "minecraft:sulfur", action, "Biome", subjectKey, "layers[0].palette[1]"); + } + + private static PackValidationResult result(List findings) { + return new PackValidationResult("overworld", List.of(), List.of(), 1L, findings, "26.1.2"); + } + + @Test + public void unknownPackIsReportedWithoutAnyCompatLines() { + List lines = CommandPack.compatOutput("nope", null); + + assertEquals(1, lines.size()); + assertTrue(lines.getFirst(), lines.getFirst().contains("No validation result for 'nope'")); + } + + @Test + public void packWithoutFindingsSaysSoAndNamesTheVersion() { + List lines = CommandPack.compatOutput("overworld", result(List.of())); + + assertEquals(1, lines.size()); + assertTrue(lines.getFirst(), lines.getFirst().contains("Pack 'overworld' uses no content")); + assertTrue(lines.getFirst(), lines.getFirst().contains("26.1.2")); + } + + @Test + public void findingsArePrintedUncappedWithTheFallbackRemedy() { + List lines = CommandPack.compatOutput("overworld", result(List.of( + block(CompatAction.EXCLUDED, "cave/a"), + block(CompatAction.EXCLUDED, "cave/b"), + block(CompatAction.EXCLUDED, "cave/c"), + block(CompatAction.EXCLUDED, "cave/d")))); + + assertEquals(3, lines.size()); + assertTrue(lines.get(0), lines.get(0).contains("Pack 'overworld': content unavailable on Minecraft 26.1.2")); + assertTrue(lines.get(1), lines.get(1).contains("minecraft:sulfur (block)")); + assertFalse("the command prints every subject", lines.get(1).contains("more")); + assertTrue(lines.get(1), lines.get(1).contains("cave/d")); + assertTrue(lines.get(2), lines.get(2).contains("blockFallbacks")); + } + + @Test + public void compatIsInvocableWithoutAPackArgument() throws Exception { + DirectorNodeDescriptor node = DirectorAnnotationCompatibility + .fromMethod(CommandPack.class.getDeclaredMethod("compat", String.class)) + .orElseThrow(); + DirectorParameterDescriptor pack = node.getParameters().getFirst(); + + assertEquals("pack", pack.getName()); + assertFalse("compat must not require a pack", pack.isRequired()); + assertFalse("Director needs a non-blank default to treat the parameter as optional", + pack.getDefaultValue().isBlank()); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java index 13194ef1a..cd4ce78e8 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedStartup.java @@ -25,6 +25,7 @@ import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidator; import art.arcane.iris.modded.command.ModdedPackCommands; +import art.arcane.iris.spi.IrisPlatforms; import net.minecraft.commands.Commands; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; @@ -41,6 +42,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; public final class ModdedStartup { + private static final int COMPAT_BOOT_KEY_CAP = 3; private static final AtomicBoolean PREPARED = new AtomicBoolean(false); private static final AtomicBoolean STARTED = new AtomicBoolean(false); @@ -138,17 +140,23 @@ public final class ModdedStartup { try { PackValidationResult result = PackValidator.validate(packDir); PackValidationRegistry.publish(result); + String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null; + String compatSummary = PackValidator.compatSummary(result, minecraftVersion); + String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary; if (!result.isLoadable()) { ModdedIrisLog.error("Iris pack '{}' FAILED validation with {} blocking error(s); world/studio creation will be refused. First error: {}", result.getPackName(), result.getBlockingErrors().size(), result.getBlockingErrors().getFirst()); } else if (!result.getWarnings().isEmpty()) { - ModdedIrisLog.info("Iris pack '{}' validated ({} warning(s)).", result.getPackName(), result.getWarnings().size()); + ModdedIrisLog.info("Iris pack '{}' validated ({} warning(s)).{}", result.getPackName(), result.getWarnings().size(), compatSuffix); for (String warning : result.getWarnings()) { ModdedIrisLog.warn(" [{}] {}", result.getPackName(), warning); } } else { - ModdedIrisLog.info("Iris pack '{}' validated.", result.getPackName()); + ModdedIrisLog.info("Iris pack '{}' validated.{}", result.getPackName(), compatSuffix); + } + for (String line : PackValidator.compatBootLines(result, minecraftVersion, COMPAT_BOOT_KEY_CAP)) { + ModdedIrisLog.warn("{}", line); } } catch (Throwable e) { ModdedIrisLog.error("Iris pack validation failed for '{}'", packDir.getName(), e); 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 719c2b0c8..f29523a17 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 @@ -164,7 +164,8 @@ final class ModdedCommandHelp { Entry.command("validate", "[pack]", ModdedHelpMessages.COMMAND_VALIDATE_VALIDATE_A_PACK_OR_EVERY_PACK, "v"), Entry.command("cleanup", " [apply]", ModdedHelpMessages.COMMAND_CLEANUP_PREVIEW_OR_QUARANTINE_UNUSED_RESOURCE_CANDIDATES, "c"), Entry.command("restore", " [apply]", ModdedHelpMessages.COMMAND_RESTORE_PREVIEW_OR_RESTORE_THE_LATEST_QUARANTINE, "r"), - Entry.command("status", "[pack]", ModdedHelpMessages.COMMAND_STATUS_SHOW_CACHED_VALIDATION_STATUS, "s") + Entry.command("status", "[pack]", ModdedHelpMessages.COMMAND_STATUS_SHOW_CACHED_VALIDATION_STATUS, "s"), + Entry.command("compat", "[pack]", ModdedHelpMessages.COMMAND_COMPAT_SHOW_CONTENT_THAT_CANNOT_GENERATE_ON_THIS_MINECRAFT_VERSION, "cp") )); SECTIONS.put("pk", SECTIONS.get("pack")); SECTIONS.put("world", List.of( diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java index 10a7dd589..fc76cdcbf 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java @@ -19,6 +19,7 @@ package art.arcane.iris.modded.command; import art.arcane.iris.modded.ModdedIrisLog; +import art.arcane.iris.core.compat.PackCompatReport; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.core.pack.PackDirectoryResolver; import art.arcane.iris.core.pack.PackResourceCleanup; @@ -93,6 +94,15 @@ public final class ModdedPackCommands { .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) .executes((CommandContext context) -> status(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("compat") + .executes((CommandContext context) -> compat(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> compat(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("cp") + .executes((CommandContext context) -> compat(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> compat(context.getSource(), StringArgumentType.getString(context, "pack"))))); + return root; } @@ -215,6 +225,77 @@ public final class ModdedPackCommands { return 1; } + private static int compat(CommandSourceStack source, String pack) { + if (pack == null || pack.isBlank()) { + Map snapshot = PackValidationRegistry.snapshot(); + if (snapshot.isEmpty()) { + IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST)); + return 0; + } + for (Map.Entry entry : snapshot.entrySet()) { + for (String line : compatOutput(entry.getKey(), entry.getValue())) { + IrisModdedCommands.ok(source, line); + } + } + return 1; + } + PackValidationResult result = PackValidationRegistry.get(pack); + for (String line : compatOutput(pack, result)) { + if (result == null) { + IrisModdedCommands.fail(source, line); + } else { + IrisModdedCommands.ok(source, line); + } + } + return result == null ? 0 : 1; + } + + /** + * The compat listing for one pack, straight from the persisted validation so no pack is reloaded. Every key is + * printed with every subject; the boot listing is the capped form of the same data. + */ + static List compatOutput(String pack, PackValidationResult result) { + List lines = new ArrayList<>(); + if (result == null) { + lines.add(IrisLanguage.plain( + ModdedCommandMessages.MODDED_PACK_COMMANDS_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, + MessageArgument.untrusted("pack", pack), + MessageArgument.untrusted("pack2", pack))); + return lines; + } + String version = compatVersion(result); + if (result.getCompatFindings().isEmpty()) { + lines.add(IrisLanguage.plain( + ModdedCommandMessages.MODDED_PACK_COMMANDS_COMPAT_NONE, + MessageArgument.untrusted("pack", pack), + MessageArgument.untrusted("version", version))); + return lines; + } + lines.add(IrisLanguage.plain( + ModdedCommandMessages.MODDED_PACK_COMMANDS_COMPAT_HEADER, + MessageArgument.untrusted("pack", pack), + MessageArgument.untrusted("version", version))); + for (String line : PackCompatReport.of(result.getCompatFindings()).keyLines(0)) { + lines.add(IrisLanguage.plain( + ModdedCommandMessages.MODDED_PACK_COMMANDS_MESSAGE_2, + MessageArgument.untrusted("value", line))); + } + lines.add(IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_COMPAT_REMEDY)); + return lines; + } + + /** The live Minecraft version, falling back to the one the validation ran against. */ + static String compatVersion(PackValidationResult result) { + if (IrisPlatforms.isBound()) { + String live = IrisPlatforms.get().minecraftVersion(); + if (live != null && !live.isBlank()) { + return live; + } + } + String persisted = result.getMinecraftVersion(); + return persisted == null || persisted.isBlank() ? "unknown" : persisted; + } + private static void report(CommandSourceStack source, PackValidationResult result) { if (result.isLoadable()) { IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_PACK_IS_LOADABLE_WARNINGS, MessageArgument.untrusted("value", result.getPackName()), MessageArgument.untrusted("value2", result.getWarnings().size()))); diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java index a73e5cb73..3e2d17ff9 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitRuntimeMessages.java @@ -158,6 +158,19 @@ public final class BukkitRuntimeMessages { "other", C.GRAY + " ... and {count} more warnings." ) ); + public static final TextKey COMMAND_PACK_COMPAT_HEADER = TextKey.of( + "iris.bukkit.runtime.commandpack.compat_header", + C.YELLOW + "Pack '" + "{pack}" + "': content unavailable on Minecraft " + "{version}" + ); + public static final TextKey COMMAND_PACK_COMPAT_NONE = TextKey.of( + "iris.bukkit.runtime.commandpack.compat_none", + C.GREEN + "Pack '" + "{pack}" + "' uses no content that is unavailable on Minecraft " + "{version}" + "." + ); + public static final TextKey COMMAND_PACK_COMPAT_REMEDY = TextKey.of( + "iris.bukkit.runtime.commandpack.compat_remedy", + C.GRAY + "Update the server to a newer Minecraft to restore this content, or declare fallbacks" + + " (dimension blockFallbacks, block backup)." + ); public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of( "iris.bukkit.runtime.commanddeveloper.update_world_warning", C.RED + "You should always make a backup before using this", @@ -644,6 +657,9 @@ public final class BukkitRuntimeMessages { COMMAND_PACK_MESSAGE, COMMAND_PACK_MESSAGE_2, COMMAND_PACK_MORE_WARNING_S, + COMMAND_PACK_COMPAT_HEADER, + COMMAND_PACK_COMPAT_NONE, + COMMAND_PACK_COMPAT_REMEDY, COMMAND_DEVELOPER_UPDATE_WORLD_WARNING, BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW, BULK_STRUCTURE_IMPORTER_FAIL_INVALID_KEY, diff --git a/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java b/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java index 08a2f8f6d..a16f93012 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/DirectorCommandMessages.java @@ -570,6 +570,14 @@ public final class DirectorCommandMessages { "iris.director.commandpack.param.pack_folder_name", "The pack folder name" ); + public static final TextKey COMMAND_PACK_DIRECTOR_SHOW_CONTENT_CANNOT_GENERATE_THIS_MINECRAFT_VERSION = TextKey.of( + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version", + "Show pack content that cannot generate on this Minecraft version" + ); + public static final TextKey COMMAND_PACK_PARAM_PACK_FOLDER_NAME_COMPAT = TextKey.of( + "iris.director.commandpack.param.pack_folder_name_compat", + "The pack folder name to report on" + ); public static final TextKey COMMAND_PREGEN_DIRECTOR_PREGENERATE_YOUR_IRIS_WORLDS = TextKey.of( "iris.director.commandpregen.director.pregenerate_your_iris_worlds", "Pregenerate your Iris worlds!" @@ -1005,6 +1013,8 @@ public final class DirectorCommandMessages { COMMAND_PACK_PARAM_PREVIEW_APPLY_2, COMMAND_PACK_DIRECTOR_SHOW_CACHED_VALIDATION_STATUS_PACK, COMMAND_PACK_PARAM_PACK_FOLDER_NAME, + COMMAND_PACK_DIRECTOR_SHOW_CONTENT_CANNOT_GENERATE_THIS_MINECRAFT_VERSION, + COMMAND_PACK_PARAM_PACK_FOLDER_NAME_COMPAT, COMMAND_PREGEN_DIRECTOR_PREGENERATE_YOUR_IRIS_WORLDS, COMMAND_PREGEN_DIRECTOR_PREGENERATE_WORLD, COMMAND_PREGEN_PARAM_RADIUS_PREGEN_BLOCKS, diff --git a/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java b/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java index caadca06f..465c74939 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java @@ -771,6 +771,19 @@ public final class ModdedCommandMessages { "iris.modded.moddedpackcommands.message_2", " ! " + "{value}" ); + public static final TextKey MODDED_PACK_COMMANDS_COMPAT_HEADER = TextKey.of( + "iris.modded.moddedpackcommands.compat_header", + "Pack '" + "{pack}" + "': content unavailable on Minecraft " + "{version}" + ); + public static final TextKey MODDED_PACK_COMMANDS_COMPAT_NONE = TextKey.of( + "iris.modded.moddedpackcommands.compat_none", + "Pack '" + "{pack}" + "' uses no content that is unavailable on Minecraft " + "{version}" + "." + ); + public static final TextKey MODDED_PACK_COMMANDS_COMPAT_REMEDY = TextKey.of( + "iris.modded.moddedpackcommands.compat_remedy", + "Update the server to a newer Minecraft to restore this content, or declare fallbacks" + + " (dimension blockFallbacks, block backup)." + ); public static final TextKey MODDED_PACK_COMMANDS_MORE_WARNING_S = TextKey.of( "iris.modded.moddedpackcommands.more_warning_s", " ... and " + "{value}" + " more warning(s)." @@ -1432,6 +1445,9 @@ public final class ModdedCommandMessages { MODDED_PACK_COMMANDS_PACK_IS_BROKEN, MODDED_PACK_COMMANDS_MESSAGE, MODDED_PACK_COMMANDS_MESSAGE_2, + MODDED_PACK_COMMANDS_COMPAT_HEADER, + MODDED_PACK_COMMANDS_COMPAT_NONE, + MODDED_PACK_COMMANDS_COMPAT_REMEDY, MODDED_PACK_COMMANDS_MORE_WARNING_S, MODDED_PACK_COMMANDS_NO_CLEANUP_CANDIDATES_FOUND_PACK, MODDED_PACK_COMMANDS_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, diff --git a/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java b/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java index 070203132..658bed468 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java @@ -278,6 +278,10 @@ public final class ModdedHelpMessages { "iris.modded.help.entry.command.status_validation", "Show cached validation status" ); + public static final TextKey COMMAND_COMPAT_SHOW_CONTENT_THAT_CANNOT_GENERATE_ON_THIS_MINECRAFT_VERSION = TextKey.of( + "iris.modded.help.entry.command.compat_content", + "Show content that cannot generate on this Minecraft version" + ); public static final TextKey COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK = TextKey.of( "iris.modded.help.entry.command.enable", "Create and inject a persistent Iris dimension at runtime; refuses if the pack is missing, quote pack:dimensionKey to pick a specific pack dimension" @@ -484,6 +488,7 @@ public final class ModdedHelpMessages { COMMAND_CLEANUP_PREVIEW_OR_QUARANTINE_UNUSED_RESOURCE_CANDIDATES, COMMAND_RESTORE_PREVIEW_OR_RESTORE_THE_LATEST_QUARANTINE, COMMAND_STATUS_SHOW_CACHED_VALIDATION_STATUS, + COMMAND_COMPAT_SHOW_CONTENT_THAT_CANNOT_GENERATE_ON_THIS_MINECRAFT_VERSION, COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, COMMAND_REPLACE_OVERWORLD_INJECT_AN_IRIS_PRIMARY_WORLD_AND_ROUTE_PLAYERS_THERE_INSTEAD_OF_THE, COMMAND_DISABLE_EVACUATE_AND_UNLOAD_AN_IRIS_DIMENSION_WORLD_DATA_ON_DISK_IS_KEPT, diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java index 522196acc..a49c7ca6b 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationCache.java @@ -1,6 +1,9 @@ package art.arcane.iris.core.pack; import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.compat.CompatAction; +import art.arcane.iris.core.compat.CompatFinding; +import art.arcane.iris.core.compat.CompatRegistry; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformRegistries; @@ -28,7 +31,7 @@ import java.util.Optional; import java.util.Set; public final class PackValidationCache { - private static final int SCHEMA_VERSION = 2; + private static final int SCHEMA_VERSION = 3; private static final long MAX_CACHE_BYTES = 16L * 1024L * 1024L; private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -105,11 +108,17 @@ public final class PackValidationCache { || !actual.add(cached.packName)) { return Optional.empty(); } + List findings = readFindings(cached.compatFindings); + if (findings == null) { + return Optional.empty(); + } results.add(new PackValidationResult( cached.packName, cached.blockingErrors, cached.warnings, - cached.validatedAtMillis)); + cached.validatedAtMillis, + findings, + cached.minecraftVersion)); } if (!expected.equals(actual)) { return Optional.empty(); @@ -144,6 +153,8 @@ public final class PackValidationCache { cached.blockingErrors = List.copyOf(result.getBlockingErrors()); cached.warnings = List.copyOf(result.getWarnings()); cached.validatedAtMillis = result.getValidatedAtMillis(); + cached.minecraftVersion = result.getMinecraftVersion(); + cached.compatFindings = writeFindings(result.getCompatFindings()); state.results.add(cached); } @@ -167,6 +178,48 @@ public final class PackValidationCache { } } + /** Null when a persisted finding is malformed; the caller discards the whole cache file. */ + private static List readFindings(List cached) { + if (cached == null) { + return List.of(); + } + List findings = new ArrayList<>(cached.size()); + for (CachedFinding finding : cached) { + if (finding == null || finding.key == null || finding.registry == null || finding.action == null) { + return null; + } + CompatRegistry registry; + CompatAction action; + try { + registry = CompatRegistry.valueOf(finding.registry); + action = CompatAction.valueOf(finding.action); + } catch (IllegalArgumentException unknownEnum) { + return null; + } + findings.add(new CompatFinding( + registry, finding.key, action, finding.subjectType, finding.subjectKey, finding.detail)); + } + return List.copyOf(findings); + } + + private static List writeFindings(List findings) { + if (findings == null || findings.isEmpty()) { + return List.of(); + } + List cached = new ArrayList<>(findings.size()); + for (CompatFinding finding : findings) { + CachedFinding entry = new CachedFinding(); + entry.registry = finding.registry().name(); + entry.key = finding.key(); + entry.action = finding.action().name(); + entry.subjectType = finding.subjectType(); + entry.subjectKey = finding.subjectKey(); + entry.detail = finding.detail(); + cached.add(entry); + } + return cached; + } + private static void updateSorted(MessageDigest digest, List values) { List sorted = new ArrayList<>(Objects.requireNonNull(values, "Pack validation registry keys")); sorted.sort(String::compareTo); @@ -197,5 +250,16 @@ public final class PackValidationCache { private List blockingErrors; private List warnings; private long validatedAtMillis; + private String minecraftVersion; + private List compatFindings; + } + + private static final class CachedFinding { + private String registry; + private String key; + private String action; + private String subjectType; + private String subjectKey; + private String detail; } } diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java index b00e2e938..a9971a69d 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java @@ -18,6 +18,8 @@ package art.arcane.iris.core.pack; +import art.arcane.iris.core.compat.CompatFinding; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,15 +29,29 @@ public final class PackValidationResult { private final List blockingErrors; private final List warnings; private final long validatedAtMillis; + private final List compatFindings; + private final String minecraftVersion; + /** Result without version-content gating data; used where the pack was never loaded through the gate. */ public PackValidationResult(String packName, List blockingErrors, List warnings, long validatedAtMillis) { + this(packName, blockingErrors, warnings, validatedAtMillis, List.of(), null); + } + + public PackValidationResult(String packName, + List blockingErrors, + List warnings, + long validatedAtMillis, + List compatFindings, + String minecraftVersion) { this.packName = packName; this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors); this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings); this.validatedAtMillis = validatedAtMillis; + this.compatFindings = compatFindings == null ? List.of() : List.copyOf(compatFindings); + this.minecraftVersion = minecraftVersion; } public String getPackName() { @@ -57,4 +73,14 @@ public final class PackValidationResult { public long getValidatedAtMillis() { return validatedAtMillis; } + + /** Every version-content gating decision recorded while this pack was validated. */ + public List getCompatFindings() { + return compatFindings; + } + + /** The raw {@code IrisPlatform.minecraftVersion()} the gate ran against, or null when it did not run. */ + public String getMinecraftVersion() { + return minecraftVersion; + } } diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index 0a70e0061..eb35842ee 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -18,9 +18,30 @@ package art.arcane.iris.core.pack; +import art.arcane.iris.core.compat.CompatFinding; +import art.arcane.iris.core.compat.PackCompatReport; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisImage; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.engine.object.matter.IrisMatterObject; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.IrisPlatforms; + import java.io.File; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.regex.Pattern; public final class PackValidator { @@ -40,6 +61,14 @@ public final class PackValidator { static final List REMOVED_WORLDGEN_FIELDS = List.of("fluidBodies"); static final List UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale"); static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); + private static final int BLOCKING_REASON_LIMIT = 3; + /** + * Binary assets, not key-bearing registrants: an object is gated through the placements that can place it + * (palette header only) and images carry no registry keys, so loading every one of them here would parse the + * whole object library for nothing. + */ + private static final Set> UNGATED_LOADERS = + Set.of(IrisObject.class, IrisMatterObject.class, IrisImage.class); private PackValidator() { } @@ -124,7 +153,184 @@ public final class PackValidator { addDistinct(ContentKeyValidator.strictContent() ? blockingErrors : warnings, contentKeys.strict()); addDistinct(warnings, contentKeys.advisory()); - return new PackValidationResult(packName, blockingErrors, warnings, validatedAt); + String minecraftVersion = validateLiveRegistries && IrisPlatforms.isBound() + ? IrisPlatforms.get().minecraftVersion() + : null; + List compatFindings = validateLiveRegistries + ? gateContent(packFolder, dimensionFiles, minecraftVersion, blockingErrors, warnings) + : List.of(); + + return new PackValidationResult( + packName, blockingErrors, warnings, validatedAt, compatFindings, minecraftVersion); + } + + /** + * Forces a complete load of the pack through {@link IrisData} so every registrant and every reachable + * {@link IrisObjectPlacement} is evaluated by the version-content gate, then folds the resulting findings into + * this validation. Runs on a detached loader that is closed again, and only on a cache miss (the caller's cache + * fingerprint already covers the Minecraft version and the registry key sets). + */ + private static List gateContent( + File packFolder, + File[] dimensionFiles, + String minecraftVersion, + List blockingErrors, + List warnings + ) { + if (!IrisPlatforms.isBound()) { + return List.of(); + } + IrisData data = null; + try { + data = IrisData.openRuntime(packFolder); + if (!data.getContentGate().ready()) { + return List.of(); + } + boolean dimensionExcluded = loadEverything(data, dimensionFiles); + List findings = data.getCompatReport().findings(); + applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings); + return findings; + } catch (Throwable e) { + IrisLogging.reportError("Version-content gating failed for pack '" + packFolder.getName() + "'", e); + return List.of(); + } finally { + if (data != null) { + try { + data.close(); + } catch (Throwable ignored) { + } + } + } + } + + /** + * Loads every registrant of every loader type (dimensions first, so dimension-level fallbacks are in place before + * anything else resolves) and evaluates every placement reachable from the dimensions, regions and biomes. + * + * @return true when any dimension in the pack cannot generate on this server + */ + private static boolean loadEverything(IrisData data, File[] dimensionFiles) { + boolean dimensionExcluded = false; + for (File dimensionFile : dimensionFiles) { + IrisDimension dimension = data.getDimensionLoader().load( + PackValidationIo.stripExtension(dimensionFile.getName()), false); + if (dimension != null && dimension.isCompatExcluded()) { + dimensionExcluded = true; + } + } + + Set placements = Collections.newSetFromMap(new IdentityHashMap<>()); + for (Map.Entry, ResourceLoader> entry + : data.getLoaders().entrySet()) { + if (UNGATED_LOADERS.contains(entry.getKey())) { + continue; + } + for (String key : entry.getValue().getPossibleKeys()) { + collectPlacements(entry.getValue().load(key, false), placements); + } + } + for (IrisObjectPlacement placement : placements) { + placement.evaluateCompat(data); + } + return dimensionExcluded; + } + + private static void collectPlacements(IrisRegistrant registrant, Set out) { + if (registrant instanceof IrisBiome biome) { + addPlacements(biome.getObjects(), out); + return; + } + if (registrant instanceof IrisRegion region) { + addPlacements(region.getObjects(), out); + } + } + + private static void addPlacements(Collection placements, Set out) { + if (placements == null) { + return; + } + for (IrisObjectPlacement placement : placements) { + if (placement != null) { + out.add(placement); + } + } + } + + /** + * Turns gate findings into validation output: an excluded dimension is blocking, and any + * {@link ContentKeyValidator} warning about a key the gate already reported is dropped so the operator does not + * read the same missing key twice. + */ + static void applyCompatFindings( + String minecraftVersion, + boolean dimensionExcluded, + List findings, + List blockingErrors, + List warnings + ) { + if (dimensionExcluded) { + addDistinct(blockingErrors, List.of(excludedDimensionError(minecraftVersion, findings))); + } + if (findings == null || findings.isEmpty() || warnings == null) { + return; + } + Set reported = new LinkedHashSet<>(); + for (CompatFinding finding : findings) { + reported.add("'" + finding.key() + "'"); + } + warnings.removeIf(warning -> { + for (String quoted : reported) { + if (warning.contains(quoted)) { + return true; + } + } + return false; + }); + } + + /** {@code Pack cannot generate on Minecraft 26.1.2: minecraft:sulfur (block), minecraft:camel (entity)} */ + static String excludedDimensionError(String minecraftVersion, List findings) { + StringBuilder sb = new StringBuilder(96); + sb.append("Pack cannot generate on Minecraft ") + .append(minecraftVersion == null || minecraftVersion.isBlank() ? "unknown" : minecraftVersion) + .append(": "); + Set reasons = new LinkedHashSet<>(); + if (findings != null) { + for (CompatFinding finding : findings) { + if (reasons.size() == BLOCKING_REASON_LIMIT) { + break; + } + reasons.add(finding.key() + " (" + finding.registry().label() + ")"); + } + } + sb.append(reasons.isEmpty() ? "the dimension composes content this server does not have" + : String.join(", ", reasons)); + return sb.toString(); + } + + /** Boot listing for a validated pack; empty when the gate found nothing. */ + public static List compatBootLines(PackValidationResult result, String minecraftVersion, int perKeyCap) { + if (result == null || result.getCompatFindings().isEmpty()) { + return List.of(); + } + return PackCompatReport.of(result.getCompatFindings()) + .bootLines(result.getPackName(), versionOf(result, minecraftVersion), perKeyCap); + } + + /** One-line compat summary appended to the {@code Pack 'x' validated (...)} line; empty when nothing to say. */ + public static String compatSummary(PackValidationResult result, String minecraftVersion) { + if (result == null || result.getCompatFindings().isEmpty()) { + return ""; + } + return PackCompatReport.of(result.getCompatFindings()) + .summaryLine(versionOf(result, minecraftVersion)); + } + + private static String versionOf(PackValidationResult result, String minecraftVersion) { + if (minecraftVersion != null && !minecraftVersion.isBlank()) { + return minecraftVersion; + } + return result.getMinecraftVersion(); } private static void addDistinct(List destination, List additions) { diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 94ce8cc04..486f431b1 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -39,6 +39,9 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisEngineData; import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.core.compat.PackCompatReport; +import art.arcane.iris.core.pack.PackValidationResult; +import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.spi.IrisLogging; import com.google.common.util.concurrent.AtomicDouble; import art.arcane.iris.spi.IrisPlatforms; @@ -66,6 +69,7 @@ import lombok.Data; import lombok.Getter; import lombok.Setter; +import java.io.File; import java.util.HashSet; import java.util.Locale; import java.util.Objects; @@ -234,6 +238,7 @@ public class IrisEngine implements Engine { EngineRuntime initialRuntime = runtimeBuilder.buildRuntime(); runtimeBuilder.publishRuntime(initialRuntime, null); IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); + logPackCompatSummary(); logStudioInitializationPhase("build_runtime", phaseStartedAt, false); phaseStartedAt = System.nanoTime(); if (requiredMode.warmGenerationCaches()) { @@ -254,6 +259,38 @@ public class IrisEngine implements Engine { IrisLogging.debug("Engine Initialized " + getCacheID()); } + /** + * One line per engine naming what this pack cannot generate on the running Minecraft version. The published + * validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack + * falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws: + * a report failure must not take an otherwise working world down with it. + */ + private void logPackCompatSummary() { + try { + File folder = getData().getDataFolder(); + // A world engine reads its snapshot copy under /iris/pack, so the dimension key is the pack's name. + String pack = getDimension().getLoadKey(); + String world = target.getWorld().name(); + String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null; + PackValidationResult published = PackValidationRegistry.get(folder.toPath()); + if (published == null) { + published = PackValidationRegistry.get(folder.getName()); + } + PackCompatReport report = published != null && !published.getCompatFindings().isEmpty() + ? PackCompatReport.of(published.getCompatFindings()) + : getData().getCompatReport(); + if (!report.isEmpty()) { + IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version)); + } + if (getDimension().isCompatExcluded()) { + IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft " + + (version == null || version.isBlank() ? "unknown" : version)); + } + } catch (Throwable e) { + IrisLogging.debug("Pack compat summary failed: " + e.getMessage()); + } + } + public void awaitGenerationCacheWarm() { if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) { return; @@ -682,7 +719,8 @@ public class IrisEngine implements Engine { return null; } - return getData().getBiomeLoader().load(getDimension().getFocus()); + IrisBiome focus = getData().getBiomeLoader().load(getDimension().getFocus()); + return focus == null || focus.isCompatExcluded() ? null : focus; } @Override @@ -691,7 +729,8 @@ public class IrisEngine implements Engine { return null; } - return getData().getRegionLoader().load(getDimension().getFocusRegion()); + IrisRegion focus = getData().getRegionLoader().load(getDimension().getFocusRegion()); + return focus == null || focus.isCompatExcluded() ? null : focus; } @Override diff --git a/core/src/main/resources/languages/de_DE.json b/core/src/main/resources/languages/de_DE.json index 60e762b83..765831e3a 100644 --- a/core/src/main/resources/languages/de_DE.json +++ b/core/src/main/resources/languages/de_DE.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Pack-Inhalte anzeigen, die in dieser Minecraft-Version nicht generiert werden können", + "iris.director.commandpack.param.pack_folder_name_compat": "Der Pack-Ordnername für den Bericht", + "iris.bukkit.runtime.commandpack.compat_header": "§ePack '{pack}': Inhalte in Minecraft {version} nicht verfügbar", + "iris.bukkit.runtime.commandpack.compat_none": "§aPack '{pack}' verwendet keine Inhalte, die in Minecraft {version} fehlen.", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7Aktualisiere den Server auf ein neueres Minecraft, um diese Inhalte wiederherzustellen, oder deklariere Ersatzwerte (Dimension blockFallbacks, Block backup).", + "iris.modded.moddedpackcommands.compat_header": "Pack '{pack}': Inhalte in Minecraft {version} nicht verfügbar", + "iris.modded.moddedpackcommands.compat_none": "Pack '{pack}' verwendet keine Inhalte, die in Minecraft {version} fehlen.", + "iris.modded.moddedpackcommands.compat_remedy": "Aktualisiere den Server auf ein neueres Minecraft, um diese Inhalte wiederherzustellen, oder deklariere Ersatzwerte (Dimension blockFallbacks, Block backup).", + "iris.modded.help.entry.command.compat_content": "Inhalte anzeigen, die in dieser Minecraft-Version nicht generiert werden können" } } diff --git a/core/src/main/resources/languages/es_ES.json b/core/src/main/resources/languages/es_ES.json index 1594a39d9..2cfd47f6a 100644 --- a/core/src/main/resources/languages/es_ES.json +++ b/core/src/main/resources/languages/es_ES.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Muestra el contenido del pack que no puede generarse en esta versión de Minecraft", + "iris.director.commandpack.param.pack_folder_name_compat": "El nombre de la carpeta del pack sobre el que informar", + "iris.bukkit.runtime.commandpack.compat_header": "§ePack '{pack}': contenido no disponible en Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aEl pack '{pack}' no usa contenido que falte en Minecraft {version}.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Pack '{pack}': contenido no disponible en Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "El pack '{pack}' no usa contenido que falte en Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Actualiza el servidor a un Minecraft más reciente para recuperar este contenido, o declara alternativas (blockFallbacks de la dimensión, backup del bloque).", + "iris.modded.help.entry.command.compat_content": "Muestra el contenido que no puede generarse en esta versión de Minecraft" } } diff --git a/core/src/main/resources/languages/fi_FI.json b/core/src/main/resources/languages/fi_FI.json index b26961ff5..2d566047b 100644 --- a/core/src/main/resources/languages/fi_FI.json +++ b/core/src/main/resources/languages/fi_FI.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Näytä paketin sisältö, jota ei voi luoda tässä Minecraft-versiossa", + "iris.director.commandpack.param.pack_folder_name_compat": "Raportoitavan pakettikansion nimi", + "iris.bukkit.runtime.commandpack.compat_header": "§ePaketti '{pack}': sisältö ei ole käytettävissä Minecraft {version} -versiossa", + "iris.bukkit.runtime.commandpack.compat_none": "§aPaketti '{pack}' ei käytä sisältöä, joka puuttuisi Minecraft {version} -versiosta.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Paketti '{pack}': sisältö ei ole käytettävissä Minecraft {version} -versiossa", + "iris.modded.moddedpackcommands.compat_none": "Paketti '{pack}' ei käytä sisältöä, joka puuttuisi Minecraft {version} -versiosta.", + "iris.modded.moddedpackcommands.compat_remedy": "Päivitä palvelin uudempaan Minecraft-versioon palauttaaksesi tämän sisällön tai määritä varasisältö (ulottuvuuden blockFallbacks, lohkon backup).", + "iris.modded.help.entry.command.compat_content": "Näytä sisältö, jota ei voi luoda tässä Minecraft-versiossa" } } diff --git a/core/src/main/resources/languages/fr_FR.json b/core/src/main/resources/languages/fr_FR.json index b017acdb3..d219a836d 100644 --- a/core/src/main/resources/languages/fr_FR.json +++ b/core/src/main/resources/languages/fr_FR.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Afficher le contenu du pack qui ne peut pas être généré sur cette version de Minecraft", + "iris.director.commandpack.param.pack_folder_name_compat": "Le nom du dossier du pack à analyser", + "iris.bukkit.runtime.commandpack.compat_header": "§ePack '{pack}' : contenu indisponible sur Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aLe pack '{pack}' n'utilise aucun contenu absent de Minecraft {version}.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Pack '{pack}' : contenu indisponible sur Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Le pack '{pack}' n'utilise aucun contenu absent de Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Mettez 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).", + "iris.modded.help.entry.command.compat_content": "Afficher le contenu qui ne peut pas être généré sur cette version de Minecraft" } } diff --git a/core/src/main/resources/languages/he_IL.json b/core/src/main/resources/languages/he_IL.json index cf3124630..19f1dc928 100644 --- a/core/src/main/resources/languages/he_IL.json +++ b/core/src/main/resources/languages/he_IL.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "הצג תוכן חבילה שלא ניתן ליצור בגרסת Minecraft הזו", + "iris.director.commandpack.param.pack_folder_name_compat": "שם תיקיית החבילה לדיווח", + "iris.bukkit.runtime.commandpack.compat_header": "§eחבילה '{pack}': תוכן לא זמין ב-Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aהחבילה '{pack}' אינה משתמשת בתוכן שחסר ב-Minecraft {version}.", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7עדכן את השרת ל-Minecraft חדש יותר כדי לשחזר את התוכן הזה, או הגדר חלופות (blockFallbacks של הממד, backup של הבלוק).", + "iris.modded.moddedpackcommands.compat_header": "חבילה '{pack}': תוכן לא זמין ב-Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "החבילה '{pack}' אינה משתמשת בתוכן שחסר ב-Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "עדכן את השרת ל-Minecraft חדש יותר כדי לשחזר את התוכן הזה, או הגדר חלופות (blockFallbacks של הממד, backup של הבלוק).", + "iris.modded.help.entry.command.compat_content": "הצג תוכן שלא ניתן ליצור בגרסת Minecraft הזו" } } diff --git a/core/src/main/resources/languages/it_IT.json b/core/src/main/resources/languages/it_IT.json index aec0c8fec..63d57a658 100644 --- a/core/src/main/resources/languages/it_IT.json +++ b/core/src/main/resources/languages/it_IT.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Mostra i contenuti del pack che non possono essere generati su questa versione di Minecraft", + "iris.director.commandpack.param.pack_folder_name_compat": "Il nome della cartella del pack da analizzare", + "iris.bukkit.runtime.commandpack.compat_header": "§ePack '{pack}': contenuti non disponibili su Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aIl pack '{pack}' non usa contenuti assenti da Minecraft {version}.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Pack '{pack}': contenuti non disponibili su Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Il pack '{pack}' non usa contenuti assenti da Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Aggiorna il server a un Minecraft più recente per ripristinare questi contenuti, oppure dichiara delle alternative (blockFallbacks della dimensione, backup del blocco).", + "iris.modded.help.entry.command.compat_content": "Mostra i contenuti che non possono essere generati su questa versione di Minecraft" } } diff --git a/core/src/main/resources/languages/ja-JP.json b/core/src/main/resources/languages/ja-JP.json index 75ac9c0cb..9421d64d1 100644 --- a/core/src/main/resources/languages/ja-JP.json +++ b/core/src/main/resources/languages/ja-JP.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "この Minecraft バージョンでは生成できないパック内容を表示します", + "iris.director.commandpack.param.pack_folder_name_compat": "対象とするパックフォルダー名", + "iris.bukkit.runtime.commandpack.compat_header": "§eパック '{pack}': Minecraft {version} では利用できない内容があります", + "iris.bukkit.runtime.commandpack.compat_none": "§aパック '{pack}' に Minecraft {version} で欠けている内容はありません。", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7この内容を復元するにはサーバーを新しい Minecraft に更新するか、代替を宣言してください (ディメンションの blockFallbacks、ブロックの backup)。", + "iris.modded.moddedpackcommands.compat_header": "パック '{pack}': Minecraft {version} では利用できない内容があります", + "iris.modded.moddedpackcommands.compat_none": "パック '{pack}' に Minecraft {version} で欠けている内容はありません。", + "iris.modded.moddedpackcommands.compat_remedy": "この内容を復元するにはサーバーを新しい Minecraft に更新するか、代替を宣言してください (ディメンションの blockFallbacks、ブロックの backup)。", + "iris.modded.help.entry.command.compat_content": "この Minecraft バージョンでは生成できない内容を表示します" } } diff --git a/core/src/main/resources/languages/ko_KR.json b/core/src/main/resources/languages/ko_KR.json index f8601297a..ed8a1ab0a 100644 --- a/core/src/main/resources/languages/ko_KR.json +++ b/core/src/main/resources/languages/ko_KR.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "이 Minecraft 버전에서 생성할 수 없는 팩 콘텐츠를 표시합니다", + "iris.director.commandpack.param.pack_folder_name_compat": "보고할 팩 폴더 이름", + "iris.bukkit.runtime.commandpack.compat_header": "§e팩 '{pack}': Minecraft {version} 에서 사용할 수 없는 콘텐츠", + "iris.bukkit.runtime.commandpack.compat_none": "§a팩 '{pack}' 은 Minecraft {version} 에 없는 콘텐츠를 사용하지 않습니다.", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7이 콘텐츠를 복원하려면 서버를 최신 Minecraft 로 업데이트하거나 대체를 선언하세요 (차원의 blockFallbacks, 블록의 backup).", + "iris.modded.moddedpackcommands.compat_header": "팩 '{pack}': Minecraft {version} 에서 사용할 수 없는 콘텐츠", + "iris.modded.moddedpackcommands.compat_none": "팩 '{pack}' 은 Minecraft {version} 에 없는 콘텐츠를 사용하지 않습니다.", + "iris.modded.moddedpackcommands.compat_remedy": "이 콘텐츠를 복원하려면 서버를 최신 Minecraft 로 업데이트하거나 대체를 선언하세요 (차원의 blockFallbacks, 블록의 backup).", + "iris.modded.help.entry.command.compat_content": "이 Minecraft 버전에서 생성할 수 없는 콘텐츠를 표시합니다" } } diff --git a/core/src/main/resources/languages/lt_LT.json b/core/src/main/resources/languages/lt_LT.json index a7940d8bf..3739fdbe7 100644 --- a/core/src/main/resources/languages/lt_LT.json +++ b/core/src/main/resources/languages/lt_LT.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Rodyti paketo turinį, kurio negalima sugeneruoti šioje Minecraft versijoje", + "iris.director.commandpack.param.pack_folder_name_compat": "Paketo aplanko pavadinimas ataskaitai", + "iris.bukkit.runtime.commandpack.compat_header": "§ePaketas '{pack}': turinys nepasiekiamas Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aPaketas '{pack}' nenaudoja turinio, kurio trūksta Minecraft {version}.", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7Atnaujinkite serverį į naujesnį Minecraft, kad atkurtumėte šį turinį, arba paskelbkite atsargas (dimensijos blockFallbacks, bloko backup).", + "iris.modded.moddedpackcommands.compat_header": "Paketas '{pack}': turinys nepasiekiamas Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Paketas '{pack}' nenaudoja turinio, kurio trūksta Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Atnaujinkite serverį į naujesnį Minecraft, kad atkurtumėte šį turinį, arba paskelbkite atsargas (dimensijos blockFallbacks, bloko backup).", + "iris.modded.help.entry.command.compat_content": "Rodyti turinį, kurio negalima sugeneruoti šioje Minecraft versijoje" } } diff --git a/core/src/main/resources/languages/nl_NL.json b/core/src/main/resources/languages/nl_NL.json index 18034346c..40bd18137 100644 --- a/core/src/main/resources/languages/nl_NL.json +++ b/core/src/main/resources/languages/nl_NL.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Toon pack-inhoud die niet kan worden gegenereerd op deze Minecraft-versie", + "iris.director.commandpack.param.pack_folder_name_compat": "De naam van de packmap om te rapporteren", + "iris.bukkit.runtime.commandpack.compat_header": "§ePack '{pack}': inhoud niet beschikbaar op Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aPack '{pack}' gebruikt geen inhoud die ontbreekt in Minecraft {version}.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Pack '{pack}': inhoud niet beschikbaar op Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Pack '{pack}' gebruikt geen inhoud die ontbreekt in Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Werk de server bij naar een nieuwere Minecraft om deze inhoud te herstellen, of declareer terugvallen (blockFallbacks van de dimensie, backup van het blok).", + "iris.modded.help.entry.command.compat_content": "Toon inhoud die niet kan worden gegenereerd op deze Minecraft-versie" } } diff --git a/core/src/main/resources/languages/pl_PL.json b/core/src/main/resources/languages/pl_PL.json index 8b30423eb..65d084b45 100644 --- a/core/src/main/resources/languages/pl_PL.json +++ b/core/src/main/resources/languages/pl_PL.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Pokaż zawartość paczki, której nie można wygenerować w tej wersji Minecraft", + "iris.director.commandpack.param.pack_folder_name_compat": "Nazwa folderu paczki do raportu", + "iris.bukkit.runtime.commandpack.compat_header": "§ePaczka '{pack}': zawartość niedostępna w Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aPaczka '{pack}' nie używa zawartości brakującej w Minecraft {version}.", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7Zaktualizuj serwer do nowszego Minecraft, aby przywrócić tę zawartość, albo zadeklaruj zamienniki (blockFallbacks wymiaru, backup bloku).", + "iris.modded.moddedpackcommands.compat_header": "Paczka '{pack}': zawartość niedostępna w Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Paczka '{pack}' nie używa zawartości brakującej w Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Zaktualizuj serwer do nowszego Minecraft, aby przywrócić tę zawartość, albo zadeklaruj zamienniki (blockFallbacks wymiaru, backup bloku).", + "iris.modded.help.entry.command.compat_content": "Pokaż zawartość, której nie można wygenerować w tej wersji Minecraft" } } diff --git a/core/src/main/resources/languages/pt_PT.json b/core/src/main/resources/languages/pt_PT.json index e6cf58cf5..1aef5e1d9 100644 --- a/core/src/main/resources/languages/pt_PT.json +++ b/core/src/main/resources/languages/pt_PT.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Mostrar o conteúdo do pack que não pode ser gerado nesta versão do Minecraft", + "iris.director.commandpack.param.pack_folder_name_compat": "O nome da pasta do pack a reportar", + "iris.bukkit.runtime.commandpack.compat_header": "§ePack '{pack}': conteúdo indisponível no Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aO pack '{pack}' não usa conteúdo em falta no Minecraft {version}.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Pack '{pack}': conteúdo indisponível no Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "O pack '{pack}' não usa conteúdo em falta no Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Atualize o servidor para um Minecraft mais recente para restaurar este conteúdo, ou declare alternativas (blockFallbacks da dimensão, backup do bloco).", + "iris.modded.help.entry.command.compat_content": "Mostrar o conteúdo que não pode ser gerado nesta versão do Minecraft" } } diff --git a/core/src/main/resources/languages/ru_RU.json b/core/src/main/resources/languages/ru_RU.json index a56d969a4..d0da17f1c 100644 --- a/core/src/main/resources/languages/ru_RU.json +++ b/core/src/main/resources/languages/ru_RU.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Показать содержимое пака, которое нельзя сгенерировать в этой версии Minecraft", + "iris.director.commandpack.param.pack_folder_name_compat": "Имя папки пака для отчёта", + "iris.bukkit.runtime.commandpack.compat_header": "§eПак '{pack}': содержимое недоступно в Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aПак '{pack}' не использует содержимое, которого нет в Minecraft {version}.", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7Обновите сервер до более новой версии Minecraft, чтобы вернуть это содержимое, или объявите замены (blockFallbacks измерения, backup блока).", + "iris.modded.moddedpackcommands.compat_header": "Пак '{pack}': содержимое недоступно в Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Пак '{pack}' не использует содержимое, которого нет в Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Обновите сервер до более новой версии Minecraft, чтобы вернуть это содержимое, или объявите замены (blockFallbacks измерения, backup блока).", + "iris.modded.help.entry.command.compat_content": "Показать содержимое, которое нельзя сгенерировать в этой версии Minecraft" } } diff --git a/core/src/main/resources/languages/tr_TR.json b/core/src/main/resources/languages/tr_TR.json index 8d264d1e3..c4dc9ff54 100644 --- a/core/src/main/resources/languages/tr_TR.json +++ b/core/src/main/resources/languages/tr_TR.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Bu Minecraft sürümünde üretilemeyen paket içeriğini göster", + "iris.director.commandpack.param.pack_folder_name_compat": "Raporlanacak paket klasörü adı", + "iris.bukkit.runtime.commandpack.compat_header": "§ePaket '{pack}': içerik Minecraft {version} sürümünde yok", + "iris.bukkit.runtime.commandpack.compat_none": "§aPaket '{pack}' Minecraft {version} sürümünde eksik olan içerik kullanmıyor.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Paket '{pack}': içerik Minecraft {version} sürümünde yok", + "iris.modded.moddedpackcommands.compat_none": "Paket '{pack}' Minecraft {version} sürümünde eksik olan içerik kullanmıyor.", + "iris.modded.moddedpackcommands.compat_remedy": "Bu 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).", + "iris.modded.help.entry.command.compat_content": "Bu Minecraft sürümünde üretilemeyen içeriği göster" } } diff --git a/core/src/main/resources/languages/vi_VI.json b/core/src/main/resources/languages/vi_VI.json index dfa29fa2c..22be9db60 100644 --- a/core/src/main/resources/languages/vi_VI.json +++ b/core/src/main/resources/languages/vi_VI.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "Hiển thị nội dung gói không thể tạo trên phiên bản Minecraft này", + "iris.director.commandpack.param.pack_folder_name_compat": "Tên thư mục gói cần báo cáo", + "iris.bukkit.runtime.commandpack.compat_header": "§eGói '{pack}': nội dung không có trên Minecraft {version}", + "iris.bukkit.runtime.commandpack.compat_none": "§aGói '{pack}' không dùng nội dung nào thiếu trên Minecraft {version}.", + "iris.bukkit.runtime.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).", + "iris.modded.moddedpackcommands.compat_header": "Gói '{pack}': nội dung không có trên Minecraft {version}", + "iris.modded.moddedpackcommands.compat_none": "Gói '{pack}' không dùng nội dung nào thiếu trên Minecraft {version}.", + "iris.modded.moddedpackcommands.compat_remedy": "Cậ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).", + "iris.modded.help.entry.command.compat_content": "Hiển thị nội dung không thể tạo trên phiên bản Minecraft này" } } diff --git a/core/src/main/resources/languages/zh_CN.json b/core/src/main/resources/languages/zh_CN.json index 72588fc7c..2839af2d8 100644 --- a/core/src/main/resources/languages/zh_CN.json +++ b/core/src/main/resources/languages/zh_CN.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "显示在当前 Minecraft 版本中无法生成的资源包内容", + "iris.director.commandpack.param.pack_folder_name_compat": "要报告的资源包文件夹名称", + "iris.bukkit.runtime.commandpack.compat_header": "§e资源包 '{pack}': 内容在 Minecraft {version} 中不可用", + "iris.bukkit.runtime.commandpack.compat_none": "§a资源包 '{pack}' 未使用 Minecraft {version} 中缺失的内容。", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7将服务器升级到更新的 Minecraft 以恢复这些内容,或声明回退 (维度的 blockFallbacks、方块的 backup)。", + "iris.modded.moddedpackcommands.compat_header": "资源包 '{pack}': 内容在 Minecraft {version} 中不可用", + "iris.modded.moddedpackcommands.compat_none": "资源包 '{pack}' 未使用 Minecraft {version} 中缺失的内容。", + "iris.modded.moddedpackcommands.compat_remedy": "将服务器升级到更新的 Minecraft 以恢复这些内容,或声明回退 (维度的 blockFallbacks、方块的 backup)。", + "iris.modded.help.entry.command.compat_content": "显示在当前 Minecraft 版本中无法生成的内容" } } diff --git a/core/src/main/resources/languages/zh_TW.json b/core/src/main/resources/languages/zh_TW.json index e27df451c..bcefc41fb 100644 --- a/core/src/main/resources/languages/zh_TW.json +++ b/core/src/main/resources/languages/zh_TW.json @@ -1550,6 +1550,15 @@ "iris.desktop.imagemap.preview_status": "X {x} Z {z} | {value} | {scale} blocks/pixel", "iris.bukkit.commandstudio.opening_image_map_studio": "§aOpening the Image Map Studio.", "iris.modded.moddedstudiocommands.image_map_requires_iris_dimension": "Stand in an active Iris or Studio dimension before opening the Image Map Studio.", - "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display." + "iris.modded.moddedstudiocommands.opening_image_map_studio_on_server_display": "Opening the Image Map Studio for {value} on the server display.", + "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version": "顯示在目前 Minecraft 版本中無法生成的資源包內容", + "iris.director.commandpack.param.pack_folder_name_compat": "要報告的資源包資料夾名稱", + "iris.bukkit.runtime.commandpack.compat_header": "§e資源包 '{pack}': 內容在 Minecraft {version} 中無法使用", + "iris.bukkit.runtime.commandpack.compat_none": "§a資源包 '{pack}' 未使用 Minecraft {version} 中缺少的內容。", + "iris.bukkit.runtime.commandpack.compat_remedy": "§7將伺服器升級到較新的 Minecraft 以還原這些內容,或宣告備援 (維度的 blockFallbacks、方塊的 backup)。", + "iris.modded.moddedpackcommands.compat_header": "資源包 '{pack}': 內容在 Minecraft {version} 中無法使用", + "iris.modded.moddedpackcommands.compat_none": "資源包 '{pack}' 未使用 Minecraft {version} 中缺少的內容。", + "iris.modded.moddedpackcommands.compat_remedy": "將伺服器升級到較新的 Minecraft 以還原這些內容,或宣告備援 (維度的 blockFallbacks、方塊的 backup)。", + "iris.modded.help.entry.command.compat_content": "顯示在目前 Minecraft 版本中無法生成的內容" } } diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidationCacheTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidationCacheTest.java index b8d7d46ae..5232eb2c9 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackValidationCacheTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidationCacheTest.java @@ -1,5 +1,8 @@ package art.arcane.iris.core.pack; +import art.arcane.iris.core.compat.CompatAction; +import art.arcane.iris.core.compat.CompatFinding; +import art.arcane.iris.core.compat.CompatRegistry; import org.junit.Rule; import org.junit.Test; import org.junit.Assume; @@ -16,6 +19,7 @@ import java.util.Optional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -61,6 +65,55 @@ public class PackValidationCacheTest { cache, "content", "context", List.of("other")).isEmpty()); } + @Test + public void cacheRoundTripsCompatFindingsAndMinecraftVersion() throws Exception { + Path cache = new File(temporaryFolder.newFolder("compat-round-trip"), "validation.json").toPath(); + CompatFinding excluded = new CompatFinding( + CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, + "Biome", "cave/sulfur-grotto", "layers[0].palette[1]"); + CompatFinding dropped = new CompatFinding( + CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, "Spawner", "desert", "spawns[0]"); + PackValidationCache.save(cache, "content", "context", List.of(new PackValidationResult( + "overworld", List.of(), List.of("warning"), 7L, List.of(excluded, dropped), "26.1.2"))); + + PackValidationResult loaded = PackValidationCache.load( + cache, "content", "context", List.of("overworld")).orElseThrow().getFirst(); + + assertEquals("26.1.2", loaded.getMinecraftVersion()); + assertEquals(List.of(excluded, dropped), loaded.getCompatFindings()); + } + + @Test + public void resultsWithoutGatingRoundTripAsEmptyFindings() throws Exception { + Path cache = new File(temporaryFolder.newFolder("compat-empty"), "validation.json").toPath(); + PackValidationCache.save(cache, "content", "context", List.of( + new PackValidationResult("overworld", List.of(), List.of(), 1L))); + + PackValidationResult loaded = PackValidationCache.load( + cache, "content", "context", List.of("overworld")).orElseThrow().getFirst(); + + assertTrue(loaded.getCompatFindings().isEmpty()); + assertNull(loaded.getMinecraftVersion()); + } + + @Test + public void cacheWithAnUnreadableFindingIsIgnored() throws Exception { + Path cache = new File(temporaryFolder.newFolder("compat-corrupt"), "validation.json").toPath(); + Files.writeString(cache, """ + { + "schemaVersion": 3, + "contentFingerprint": "content", + "contextFingerprint": "context", + "results": [ + {"packName":"overworld","blockingErrors":[],"warnings":[],"validatedAtMillis":1, + "compatFindings":[{"registry":"NOT_A_REGISTRY","key":"minecraft:sulfur","action":"EXCLUDED"}]} + ] + } + """, StandardCharsets.UTF_8); + + assertTrue(PackValidationCache.load(cache, "content", "context", List.of("overworld")).isEmpty()); + } + @Test public void corruptCacheIsIgnored() throws Exception { Path cache = new File(temporaryFolder.newFolder("corrupt"), "validation.json").toPath(); @@ -77,7 +130,7 @@ public class PackValidationCacheTest { Path cache = new File(temporaryFolder.newFolder("duplicate"), "validation.json").toPath(); Files.writeString(cache, """ { - "schemaVersion": 2, + "schemaVersion": 3, "contentFingerprint": "content", "contextFingerprint": "context", "results": [ diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCompatTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCompatTest.java new file mode 100644 index 000000000..0ed5151a2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pack/PackValidatorCompatTest.java @@ -0,0 +1,130 @@ +package art.arcane.iris.core.pack; + +import art.arcane.iris.core.compat.CompatAction; +import art.arcane.iris.core.compat.CompatFinding; +import art.arcane.iris.core.compat.CompatRegistry; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PackValidatorCompatTest { + private static CompatFinding block(CompatAction action, String subjectKey) { + return new CompatFinding( + CompatRegistry.BLOCK, "minecraft:sulfur", action, "Biome", subjectKey, "layers[0].palette[1]"); + } + + private static CompatFinding entity(CompatAction action, String subjectKey) { + return new CompatFinding( + CompatRegistry.ENTITY, "minecraft:camel", action, "Spawner", subjectKey, "spawns[0]"); + } + + @Test + public void excludedDimensionBecomesABlockingError() { + List blockingErrors = new ArrayList<>(); + List warnings = new ArrayList<>(); + + PackValidator.applyCompatFindings( + "26.1.2", + true, + List.of(block(CompatAction.EXCLUDED, "cave/sulfur"), entity(CompatAction.DROPPED, "desert")), + blockingErrors, + warnings); + + assertEquals(1, blockingErrors.size()); + assertEquals( + "Pack cannot generate on Minecraft 26.1.2: minecraft:sulfur (block), minecraft:camel (entity)", + blockingErrors.getFirst()); + } + + @Test + public void unexcludedDimensionAddsNoBlockingError() { + List blockingErrors = new ArrayList<>(); + List warnings = new ArrayList<>(); + + PackValidator.applyCompatFindings( + "26.1.2", false, List.of(block(CompatAction.DROPPED, "cave/sulfur")), blockingErrors, warnings); + + assertTrue(blockingErrors.toString(), blockingErrors.isEmpty()); + } + + @Test + public void excludedDimensionWithoutFindingsStillNamesTheVersion() { + List blockingErrors = new ArrayList<>(); + + PackValidator.applyCompatFindings("26.1.2", true, List.of(), blockingErrors, new ArrayList<>()); + + assertEquals( + "Pack cannot generate on Minecraft 26.1.2: the dimension composes content this server does not have", + blockingErrors.getFirst()); + assertTrue(PackValidator.excludedDimensionError(null, List.of()).contains("Minecraft unknown")); + } + + @Test + public void contentKeyWarningsForGatedKeysAreSuppressed() { + List warnings = new ArrayList<>(List.of( + "Unknown block key 'minecraft:sulfur' (missing from the block registry)", + "Unknown entity key 'minecraft:camel' (missing from the entity registry)", + "Unknown block key 'minecraft:cheese' (missing from the block registry)", + "Duplicate generator 'overworld'")); + + PackValidator.applyCompatFindings( + "26.1.2", + false, + List.of(block(CompatAction.EXCLUDED, "cave/sulfur"), entity(CompatAction.DROPPED, "desert")), + new ArrayList<>(), + warnings); + + assertEquals(List.of( + "Unknown block key 'minecraft:cheese' (missing from the block registry)", + "Duplicate generator 'overworld'"), warnings); + } + + @Test + public void bootLinesCapSubjectsPerKeyAndSummaryCountsActions() { + PackValidationResult result = new PackValidationResult( + "overworld", + List.of(), + List.of(), + 1L, + List.of( + block(CompatAction.EXCLUDED, "cave/sulfur-grotto"), + block(CompatAction.EXCLUDED, "desert/sulfur-flats"), + block(CompatAction.DROPPED, "cave/sulfur-grotto"), + block(CompatAction.SUBSTITUTED, "desert/dunes"), + entity(CompatAction.EXCLUDED, "camel")), + "26.1.2"); + + List lines = PackValidator.compatBootLines(result, null, 3); + + assertEquals(4, lines.size()); + assertEquals("Pack 'overworld': content unavailable on Minecraft 26.1.2", lines.get(0)); + assertTrue(lines.get(1), lines.get(1).startsWith(" minecraft:sulfur (block): ")); + assertTrue(lines.get(1), lines.get(1).endsWith("; +1 more")); + assertEquals(3, lines.get(1).split("; ").length - 1); + assertTrue(lines.get(2), lines.get(2).startsWith(" minecraft:camel (entity): ")); + assertFalse(lines.get(2), lines.get(2).contains("more")); + assertTrue(lines.get(3), lines.get(3).contains("/iris pack compat overworld")); + + assertEquals( + "2 content keys unavailable on Minecraft 26.1.2: 3 excluded, 1 dropped, 1 substituted.", + PackValidator.compatSummary(result, null)); + assertEquals( + "2 content keys unavailable on Minecraft 26.2: 3 excluded, 1 dropped, 1 substituted.", + PackValidator.compatSummary(result, "26.2")); + } + + @Test + public void bootLinesAndSummaryAreEmptyWithoutFindings() { + PackValidationResult result = new PackValidationResult("overworld", List.of(), List.of(), 1L); + + assertTrue(PackValidator.compatBootLines(result, "26.1.2", 3).isEmpty()); + assertEquals("", PackValidator.compatSummary(result, "26.1.2")); + assertTrue(PackValidator.compatBootLines(null, "26.1.2", 3).isEmpty()); + assertEquals("", PackValidator.compatSummary(null, "26.1.2")); + } +}