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 committed 2026-09-03 04:54:38 -04:00
1 parent 0d464aabf3
commit 0b543bf7ee
33 files changed
+1018 -28

No files matched your search

@@ -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());
}
}