Compat report, validation and commands

- Force-load every registrant and placement during pack validation and persist the findings with the Minecraft version in the validation cache
- Turn an excluded dimension into a blocking validation error and suppress content-key warnings the gate already reported
- Print the capped per-key listing after the validation lines on Bukkit and modded, ordered excluded, dropped, substituted
- Log one summary line per world engine from the published validation result
- Add /iris pack compat on Bukkit and modded with help and locale keys for every shipped language
This commit is contained in:
Brian Neumann-Fopiano
2026-09-03 04:54:38 -04:00
parent 0d464aabf3
commit 0b543bf7ee
33 changed files with 1018 additions and 28 deletions
@@ -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();
@@ -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<String, PackValidationResult> 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<String, PackValidationResult> 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<String> compatOutput(String pack, PackValidationResult result) {
List<String> 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);
@@ -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<CompatFinding> findings) {
return new PackValidationResult("overworld", List.of(), List.of(), 1L, findings, "26.1.2");
}
@Test
public void unknownPackIsReportedWithoutAnyCompatLines() {
List<String> 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<String> 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<String> 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());
}
}
@@ -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);
@@ -164,7 +164,8 @@ final class ModdedCommandHelp {
Entry.command("validate", "[pack]", ModdedHelpMessages.COMMAND_VALIDATE_VALIDATE_A_PACK_OR_EVERY_PACK, "v"),
Entry.command("cleanup", "<pack> [apply]", ModdedHelpMessages.COMMAND_CLEANUP_PREVIEW_OR_QUARANTINE_UNUSED_RESOURCE_CANDIDATES, "c"),
Entry.command("restore", "<pack> [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(
@@ -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<CommandSourceStack> context) -> status(context.getSource(), StringArgumentType.getString(context, "pack")))));
root.then(Commands.literal("compat")
.executes((CommandContext<CommandSourceStack> context) -> compat(context.getSource(), null))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> compat(context.getSource(), StringArgumentType.getString(context, "pack")))));
root.then(Commands.literal("cp")
.executes((CommandContext<CommandSourceStack> context) -> compat(context.getSource(), null))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> 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<String, PackValidationResult> 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<String, PackValidationResult> 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<String> compatOutput(String pack, PackValidationResult result) {
List<String> 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())));
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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<CompatFinding> 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<CompatFinding> readFindings(List<CachedFinding> cached) {
if (cached == null) {
return List.of();
}
List<CompatFinding> 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<CachedFinding> writeFindings(List<CompatFinding> findings) {
if (findings == null || findings.isEmpty()) {
return List.of();
}
List<CachedFinding> 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<String> values) {
List<String> sorted = new ArrayList<>(Objects.requireNonNull(values, "Pack validation registry keys"));
sorted.sort(String::compareTo);
@@ -197,5 +250,16 @@ public final class PackValidationCache {
private List<String> blockingErrors;
private List<String> warnings;
private long validatedAtMillis;
private String minecraftVersion;
private List<CachedFinding> compatFindings;
}
private static final class CachedFinding {
private String registry;
private String key;
private String action;
private String subjectType;
private String subjectKey;
private String detail;
}
}
@@ -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<String> blockingErrors;
private final List<String> warnings;
private final long validatedAtMillis;
private final List<CompatFinding> 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<String> blockingErrors,
List<String> warnings,
long validatedAtMillis) {
this(packName, blockingErrors, warnings, validatedAtMillis, List.of(), null);
}
public PackValidationResult(String packName,
List<String> blockingErrors,
List<String> warnings,
long validatedAtMillis,
List<CompatFinding> 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<CompatFinding> getCompatFindings() {
return compatFindings;
}
/** The raw {@code IrisPlatform.minecraftVersion()} the gate ran against, or null when it did not run. */
public String getMinecraftVersion() {
return minecraftVersion;
}
}
@@ -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<String> REMOVED_WORLDGEN_FIELDS = List.of("fluidBodies");
static final List<String> 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<Class<?>> 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<CompatFinding> 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<CompatFinding> gateContent(
File packFolder,
File[] dimensionFiles,
String minecraftVersion,
List<String> blockingErrors,
List<String> 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<CompatFinding> 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<IrisObjectPlacement> placements = Collections.newSetFromMap(new IdentityHashMap<>());
for (Map.Entry<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> 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<IrisObjectPlacement> 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<IrisObjectPlacement> placements, Set<IrisObjectPlacement> 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<CompatFinding> findings,
List<String> blockingErrors,
List<String> warnings
) {
if (dimensionExcluded) {
addDistinct(blockingErrors, List.of(excludedDimensionError(minecraftVersion, findings)));
}
if (findings == null || findings.isEmpty() || warnings == null) {
return;
}
Set<String> 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<CompatFinding> findings) {
StringBuilder sb = new StringBuilder(96);
sb.append("Pack cannot generate on Minecraft ")
.append(minecraftVersion == null || minecraftVersion.isBlank() ? "unknown" : minecraftVersion)
.append(": ");
Set<String> 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<String> 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<String> destination, List<String> additions) {
@@ -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 <world>/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
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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 הזו"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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 バージョンでは生成できない内容を表示します"
}
}
+10 -1
View File
@@ -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 버전에서 생성할 수 없는 콘텐츠를 표시합니다"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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"
}
}
+10 -1
View File
@@ -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 版本中无法生成的内容"
}
}
+10 -1
View File
@@ -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 版本中無法生成的內容"
}
}
@@ -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": [
@@ -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<String> blockingErrors = new ArrayList<>();
List<String> 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<String> blockingErrors = new ArrayList<>();
List<String> 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<String> 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<String> 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<String> 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"));
}
}