Semantic registry validation

- Generated registry validation always re-renders recorded semantics with the current fixer
- Validation canonicalizes the rendering like the installer
- Renderer changes need no version bump
- The renderer identity SPI, both identity strings and the identity-based source preference are gone
- New captures record the fixer class as an informational renderer identity
- The v1 history format is unchanged
- The modded forced datapack, world check and datapack command use the runtime data version
- That is the same data version validation uses
- Tests cover a renderer change under an unchanged identity
- Tests cover 26.3 bare natural_mob_spawns history against the overlay rendering
- Tests cover semantic round trips for every supported fixer
This commit is contained in:
Brian Neumann-Fopiano
2026-09-29 05:41:18 -04:00
parent aa428c0383
commit 3e4a2432bc
16 changed files with 409 additions and 207 deletions
@@ -15,7 +15,6 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockResolver;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeGenerationRegistryImpl;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -49,7 +48,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
return access;
}
throw new IllegalStateException("Generation registry contract requires loaded native registries");
}), "bukkit-generation-registry-v1", new NativeWorldGenerationImpl().generationRendererIdentity());
}), "bukkit-generation-registry-v1");
nativeAccess = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(registries,
() -> { throw new UnsupportedOperationException("Headless reloadable registries are not loaded"); },
message -> { throw new IllegalStateException("Unavailable native registry: " + message); }));
@@ -0,0 +1,156 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.world.history.GenerationRegistryContract;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public final class BareMobSpawnHistoryTest {
private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1";
private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
new GenerationRegistryContract.PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"iris:biomes/" + "c".repeat(64)
);
private static HeadlessNativeRegistries loaded;
private static PlatformGenerationRegistry registry;
@BeforeClass
public static void installOverlayRendering() throws IOException {
assertNull(Bukkit.getServer());
loaded = HeadlessNativeRegistries.load(Map.of(
"data/iris/worldgen/biome/biomes/" + "c".repeat(64) + ".json",
spawningBiome().generateJson(new DataFixerV263())
));
registry = new HeadlessNativePlatform(new File("unused-native-platform"), loaded::registries)
.generationRegistry();
}
@AfterClass
public static void closeRegistries() throws IOException {
if (loaded != null) {
loaded.close();
}
}
@Test
public void bareMobSpawnHistoryValidatesAgainstTheInstalledOverlayRendering() throws IOException {
GenerationRegistryContract legacy = legacyContract(INSTALLED, spawningBiome());
GenerationRegistryContract.GeneratedSource source = legacy.generatedSources().get(INSTALLED);
assertEquals(GenerationRegistryContract.FINGERPRINT_SCHEMA_VERSION_ONE, legacy.fingerprintSchema());
assertEquals(LEGACY_RENDERER_IDENTITY, source.rendererIdentity());
assertNotEquals(
JsonParser.parseString(source.sourceJson()),
JsonParser.parseString(registry.generatedDefinition(
INSTALLED.registryKey(),
INSTALLED.resourceKey()
).value())
);
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(legacy),
new DataFixerV263(),
registry
);
legacy.requireDefinitionsAvailableIn(available);
}
@Test
public void bareMobSpawnHistoryStillRejectsDifferentSemantics() throws IOException {
GenerationRegistryContract changed = legacyContract(INSTALLED, spawningBiome().setTemperature(0.2D));
IOException failure = assertThrows(IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(changed),
new DataFixerV263(),
registry
));
assertEquals("Historical generated registry definition changed for minecraft:worldgen/biome / "
+ INSTALLED.resourceKey() + ".", failure.getMessage());
}
@Test
public void bareMobSpawnHistoryStillRejectsAMissingDefinition() throws IOException {
GenerationRegistryContract missing = legacyContract(new GenerationRegistryContract.PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"iris:biomes/" + "d".repeat(64)
), spawningBiome());
assertThrows(IOException.class, () -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(missing),
new DataFixerV263(),
registry
));
}
private static GenerationRegistryContract legacyContract(
GenerationRegistryContract.PhysicalResourceKey key,
IrisBiomeCustom biome
) {
JsonObject rendered = JsonParser.parseString(biome.generateJson(new DataFixerV263())).getAsJsonObject();
JsonObject attributes = rendered.getAsJsonObject("attributes");
attributes.add(NATURAL_MOB_SPAWNS, attributes.getAsJsonObject(NATURAL_MOB_SPAWNS).get("argument"));
String captured = registry.canonicalDefinition(key.registryKey(), key.resourceKey(), rendered.toString())
.value();
String semantic = GenerationRegistryContractFactory.customBiomeEffectiveSemanticJson(biome, null);
GenerationRegistryContract.GeneratedSource source = new GenerationRegistryContract.GeneratedSource(
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
GenerationRegistryContractFactory.fingerprintCustomBiomeAuthoredDefinition(semantic),
semantic,
LEGACY_RENDERER_IDENTITY,
GenerationRegistryContractFactory.fingerprintDefinition(
key,
PlatformGenerationRegistry.Definition.exactJson(captured),
"generated"
),
captured
);
return GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, source)),
Map.of(key, source)
);
}
private static IrisBiomeCustom spawningBiome() {
return new IrisBiomeCustom()
.setId("tester")
.setSpawnRarity(2)
.setSpawns(new KList<>(
new IrisBiomeCustomSpawn()
.setType("minecraft:zombie")
.setGroup(IrisBiomeCustomSpawnType.MONSTER)
.setMinCount(2)
.setMaxCount(4)
.setWeight(7),
new IrisBiomeCustomSpawn()
.setType("minecraft:cow")
.setGroup(IrisBiomeCustomSpawnType.CREATURE)
.setMinCount(3)
.setMaxCount(3)
.setWeight(5)
));
}
}
@@ -1,7 +1,6 @@
package art.arcane.iris.probe;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeWorldGenerationImpl;
import net.minecraft.data.registries.VanillaRegistries;
import org.bukkit.Bukkit;
import org.junit.Test;
@@ -22,8 +21,6 @@ public final class HeadlessGenerationRegistryTest {
HeadlessNativeTestRegistries::get);
PlatformGenerationRegistry registry = platform.generationRegistry();
assertEquals("bukkit-generation-registry-v1", registry.runtimeIdentity());
assertEquals(new NativeWorldGenerationImpl().generationRendererIdentity(),
registry.generatedDefinitionRendererIdentity());
assertEquals(registry.canonicalDefinition("minecraft:worldgen/biome", "iris:native_test", biome),
registry.generatedDefinition("minecraft:worldgen/biome", "iris:native_test"));
assertEquals(registry.canonicalDefinition("minecraft:dimension_type", "iris:native_test", dimension),
@@ -314,12 +314,13 @@ public final class ModdedForcedDatapack {
} catch (NoSuchAlgorithmException missing) {
throw new IllegalStateException("SHA-256 is unavailable", missing);
}
DataVersion dataVersion = DataVersion.getRuntime();
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
+ '|' + DataVersion.V26_3.getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
+ '|' + dataVersion.getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
compilationPackRoots(),
List.of(),
DataVersion.V26_3.get(),
dataVersion.get(),
false
);
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
@@ -328,7 +329,7 @@ public final class ModdedForcedDatapack {
private static void writeStagedPack(Path stagingDirectory) throws IOException {
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
IDataFixer fixer = DataVersion.V26_3.get();
IDataFixer fixer = DataVersion.getRuntime().get();
int packCount = 0;
KList<String> presetIds = new KList<>();
@@ -679,7 +680,7 @@ public final class ModdedForcedDatapack {
}
static void writePackMeta(Path packDirectory) throws IOException {
int packFormat = DataVersion.V26_3.getPackFormat();
int packFormat = DataVersion.getRuntime().getPackFormat();
String json = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n"
@@ -9,12 +9,10 @@ import java.util.Objects;
final class ModdedGenerationRegistry implements PlatformGenerationRegistry {
private final NativeGenerationRegistry nativeRegistry;
private final String runtimeIdentity;
private final String rendererIdentity;
ModdedGenerationRegistry(NativeGenerationRegistry nativeRegistry, Identity identity) {
ModdedGenerationRegistry(NativeGenerationRegistry nativeRegistry, String runtimeIdentity) {
this.nativeRegistry = Objects.requireNonNull(nativeRegistry, "nativeRegistry");
this.runtimeIdentity = requireText(identity.runtime(), "runtimeIdentity");
this.rendererIdentity = requireText(identity.renderer(), "rendererIdentity");
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
}
@Override
@@ -22,11 +20,6 @@ final class ModdedGenerationRegistry implements PlatformGenerationRegistry {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return rendererIdentity;
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
@@ -75,7 +68,4 @@ final class ModdedGenerationRegistry implements PlatformGenerationRegistry {
}
return required;
}
record Identity(String runtime, String renderer) {
}
}
@@ -357,8 +357,7 @@ public final class ModdedPlatform implements IrisPlatform {
throw new IllegalStateException("Minecraft server is not ready for generation registry capture.");
}
return new ModdedGenerationRegistry(NativeModdedServer.registryDefinitions(ModdedEngineBootstrap::currentServer),
new ModdedGenerationRegistry.Identity("modded-generation-registry-v1",
"modded-generated-registry-json-v1|minecraft:" + BuildConstants.MINECRAFT_VERSION));
"modded-generation-registry-v1");
}
}
@@ -55,7 +55,7 @@ final class WorldCheckDimensionContract {
}
static Dimension expectedDimensionContract(IrisDimension dimension) {
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.V26_3.get()));
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getRuntime().get()));
return new Dimension(
json.getInt("min_y"),
json.getInt("height"),
@@ -165,7 +165,7 @@ public final class ModdedDatapackCommands {
IrisDimension dimension = engine.getDimension();
String json;
try {
json = dimension.getDimensionType().toJson(DataVersion.V26_3.get());
json = dimension.getDimensionType().toJson(DataVersion.getRuntime().get());
} catch (Throwable e) {
ModdedIrisLog.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_DATAPACK_COMMANDS_DIMENSION_TYPE_GENERATION_FAILED, MessageArgument.untrusted("value", level.name()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
@@ -187,7 +187,7 @@ public final class ModdedDatapackCommands {
}
File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta");
int packFormat = DataVersion.V26_3.getPackFormat();
int packFormat = DataVersion.getRuntime().getPackFormat();
String meta = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n"
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.v1217.DataFixerV1217;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationRegistryContract;
@@ -27,6 +28,8 @@ import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import art.arcane.iris.generation.biome.IrisCustomBiomeAliasResolver;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionType;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import java.lang.reflect.Constructor;
import java.util.Set;
@@ -48,6 +51,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ModdedForcedDatapackTest {
@Test
@@ -65,6 +70,29 @@ public class ModdedForcedDatapackTest {
}
}
@Test
public void metadataFollowsTheRuntimeDataVersionThatValidationUses() throws IOException {
IrisPlatform previous = IrisPlatforms.getOrNull();
IrisPlatform runtime = mock(IrisPlatform.class);
when(runtime.minecraftVersion()).thenReturn("26.2");
Path packDirectory = Files.createTempDirectory("iris-modded-runtime-pack-format");
IrisPlatforms.unbind();
IrisPlatforms.bind(runtime);
try {
ModdedForcedDatapack.writePackMeta(packDirectory);
JSONObject pack = new JSONObject(Files.readString(packDirectory.resolve("pack.mcmeta")))
.getJSONObject("pack");
assertEquals(DataVersion.getRuntime().getPackFormat(), pack.getInt("pack_format"));
assertEquals(DataVersion.V26_2.getPackFormat(), pack.getInt("pack_format"));
} finally {
IrisPlatforms.unbind();
if (previous != null) {
IrisPlatforms.bind(previous);
}
deleteTree(packDirectory);
}
}
@Test
public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception {
DataFixerV1217 fixer = new DataFixerV1217();
@@ -55,7 +55,7 @@ public final class BukkitBinding implements INMSBinding {
@Override
public PlatformGenerationRegistry generationRegistry() {
return new BukkitGenerationRegistry(registry, "bukkit-generation-registry-v1", generation.generationRendererIdentity());
return new BukkitGenerationRegistry(registry, "bukkit-generation-registry-v1");
}
@Override
@@ -9,13 +9,11 @@ import java.util.Objects;
public record BukkitGenerationRegistry(
NativeGenerationRegistry registry,
String runtimeIdentity,
String generatedDefinitionRendererIdentity
String runtimeIdentity
) implements PlatformGenerationRegistry {
public BukkitGenerationRegistry {
registry = Objects.requireNonNull(registry, "registry");
runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
generatedDefinitionRendererIdentity = requireText(generatedDefinitionRendererIdentity, "rendererIdentity");
}
@Override
@@ -552,15 +552,14 @@ public final class GenerationRegistryContractFactory {
for (Map.Entry<GenerationRegistryContract.PhysicalResourceKey, GenerationRegistryContract.GeneratedSource>
entry
: requiredContract.generatedSources().entrySet()) {
GenerationRegistryContract.GeneratedSource previous = generatedSources.get(entry.getKey());
GenerationRegistryContract.GeneratedSource previous = generatedSources.putIfAbsent(
entry.getKey(),
entry.getValue()
);
if (previous != null && !sameGeneratedSemantic(previous, entry.getValue())) {
throw new IOException("Retained generation epochs require conflicting generated sources for "
+ entry.getKey().registryKey() + " / " + entry.getKey().resourceKey() + ".");
}
generatedSources.put(
entry.getKey(),
preferredGeneratedSource(previous, entry.getValue(), registry)
);
}
}
Map<GenerationRegistryContract.PhysicalResourceKey, String> captured = new TreeMap<>();
@@ -596,18 +595,6 @@ public final class GenerationRegistryContractFactory {
return GenerationRegistryContract.fromDefinitions(captured);
}
public static String requireGeneratedSource(
GenerationRegistryContract contract,
GenerationRegistryContract.PhysicalResourceKey key
) throws IOException {
return requireGeneratedSource(
contract,
key,
DataVersion.getRuntime().get(),
IrisPlatforms.get().registries().generationRegistry()
);
}
public static String renderGeneratedSource(
GenerationRegistryContract contract,
GenerationRegistryContract.PhysicalResourceKey key,
@@ -622,68 +609,9 @@ public final class GenerationRegistryContractFactory {
+ requiredKey.registryKey() + " / " + requiredKey.resourceKey() + ".");
}
requireGeneratedSemanticFingerprint(requiredKey, source, requiredContract.definitions().get(requiredKey));
String sourceFingerprint = fingerprintDefinition(requiredKey,
PlatformGenerationRegistry.Definition.exactJson(source.sourceJson()), "generated");
if (!source.renderedDefinitionSha256().equals(sourceFingerprint)) {
throw new IOException("Historical generated registry source changed for "
+ requiredKey.registryKey() + " / " + requiredKey.resourceKey() + ".");
}
return renderGeneratedSemantic(requiredKey, source, requiredFixer);
}
public static String requireGeneratedSource(
GenerationRegistryContract contract,
GenerationRegistryContract.PhysicalResourceKey key,
IDataFixer fixer
) throws IOException {
return requireGeneratedSource(
contract,
key,
fixer,
IrisPlatforms.get().registries().generationRegistry()
);
}
public static String requireGeneratedSource(
GenerationRegistryContract contract,
GenerationRegistryContract.PhysicalResourceKey key,
PlatformGenerationRegistry generationRegistry
) throws IOException {
return requireGeneratedSource(
contract,
key,
DataVersion.getRuntime().get(),
generationRegistry
);
}
public static String requireGeneratedSource(
GenerationRegistryContract contract,
GenerationRegistryContract.PhysicalResourceKey key,
IDataFixer fixer,
PlatformGenerationRegistry generationRegistry
) throws IOException {
GenerationRegistryContract requiredContract = Objects.requireNonNull(contract, "contract");
GenerationRegistryContract.PhysicalResourceKey requiredKey = Objects.requireNonNull(key, "key");
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
PlatformGenerationRegistry registry = Objects.requireNonNull(
generationRegistry,
"generationRegistry"
);
GenerationRegistryContract.GeneratedSource source = requiredContract.generatedSources().get(requiredKey);
if (source == null) {
throw new IOException("Historical generated registry source is missing for "
+ requiredKey.registryKey() + " / " + requiredKey.resourceKey() + ".");
}
String expectedFingerprint = requiredContract.definitions().get(requiredKey);
if (expectedFingerprint == null) {
throw new IOException("Historical generated registry definition is missing for "
+ requiredKey.registryKey() + " / " + requiredKey.resourceKey() + ".");
}
requireGeneratedSemanticFingerprint(requiredKey, source, expectedFingerprint);
return canonicalGeneratedDefinition(requiredKey, source, requiredFixer, registry).value();
}
public static String fingerprintDefinition(
GenerationRegistryContract.PhysicalResourceKey key,
PlatformGenerationRegistry.Definition definition,
@@ -783,7 +711,7 @@ public final class GenerationRegistryContractFactory {
DIMENSION_TYPE_EFFECTIVE_SOURCE_SCHEMA,
GenerationEpochContractFactory.fingerprintDimensionType(dimension.getDimensionType()),
GenerationEpochContractFactory.dimensionTypeSemanticJson(dimension.getDimensionType()),
registry.generatedDefinitionRendererIdentity(),
rendererIdentity(fixer),
canonicalDefinition
);
}
@@ -833,7 +761,7 @@ public final class GenerationRegistryContractFactory {
CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
generated.semanticFingerprint(),
generated.semanticJson(),
registry.generatedDefinitionRendererIdentity(),
rendererIdentity(fixer),
generated.definition()
);
Set<String> tags = biomeTags.computeIfAbsent(generated.physicalKey().resourceKey(), ignored -> new TreeSet<>());
@@ -882,7 +810,7 @@ public final class GenerationRegistryContractFactory {
CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
generated.semanticFingerprint(),
generated.semanticJson(),
registry.generatedDefinitionRendererIdentity(),
rendererIdentity(fixer),
generated.definition()
);
}
@@ -1087,32 +1015,9 @@ public final class GenerationRegistryContractFactory {
}
}
private static GenerationRegistryContract.GeneratedSource preferredGeneratedSource(
GenerationRegistryContract.GeneratedSource previous,
GenerationRegistryContract.GeneratedSource candidate,
PlatformGenerationRegistry registry
) {
if (previous == null) {
return candidate;
}
String currentRenderer = requireText(
registry.generatedDefinitionRendererIdentity(),
"Generated registry renderer identity"
);
boolean previousMatches = previous.rendererIdentity().equals(currentRenderer);
boolean candidateMatches = candidate.rendererIdentity().equals(currentRenderer);
if (previousMatches != candidateMatches) {
return candidateMatches ? candidate : previous;
}
return generatedSourceOrder(candidate).compareTo(generatedSourceOrder(previous)) < 0
? candidate
: previous;
}
private static String generatedSourceOrder(GenerationRegistryContract.GeneratedSource source) {
return source.rendererIdentity() + '\u0000'
+ source.renderedDefinitionSha256() + '\u0000'
+ source.sourceJson();
// Recorded for diagnostics only. Validation always re-renders the semantics with the current fixer.
private static String rendererIdentity(IDataFixer fixer) {
return fixer.getClass().getName();
}
private static boolean sameGeneratedSemantic(
@@ -1130,36 +1035,19 @@ public final class GenerationRegistryContractFactory {
IDataFixer fixer,
PlatformGenerationRegistry registry
) throws IOException {
String currentRenderer = requireText(
registry.generatedDefinitionRendererIdentity(),
"Generated registry renderer identity"
);
String renderedSource;
if (source.rendererIdentity().equals(currentRenderer)) {
renderedSource = source.sourceJson();
} else {
renderedSource = renderGeneratedSemantic(key, source, fixer);
}
PlatformGenerationRegistry.Definition definition;
String renderedSource = renderGeneratedSemantic(key, source, fixer);
try {
definition = registry.canonicalDefinition(
PlatformGenerationRegistry.Definition definition = registry.canonicalDefinition(
key.registryKey(),
key.resourceKey(),
renderedSource
);
requireGeneratedDefinition(definition, "historical generated registry definition");
return definition;
} catch (RuntimeException failure) {
throw new IOException("Historical generated registry source is incompatible for "
+ key.registryKey() + " / " + key.resourceKey() + ".", failure);
}
if (source.rendererIdentity().equals(currentRenderer)) {
String renderedFingerprint = fingerprintDefinition(key, definition, "generated");
if (!source.renderedDefinitionSha256().equals(renderedFingerprint)) {
throw new IOException("Historical generated registry source changed for "
+ key.registryKey() + " / " + key.resourceKey() + ".");
}
}
return definition;
}
private static String renderGeneratedSemantic(
@@ -675,11 +675,6 @@ public class IrisDatapackCompilerTest {
return "bootstrap-test";
}
@Override
public String generatedDefinitionRendererIdentity() {
return "bootstrap-renderer-v1";
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + dimensionTypeKey;
@@ -120,11 +120,6 @@ public class ArchivedRegistryCompilerTest {
return "bootstrap-test";
}
@Override
public String generatedDefinitionRendererIdentity() {
return "bootstrap-renderer-v1";
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + dimensionTypeKey;
@@ -8,7 +8,11 @@ import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.IDataFixer;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisBiomeCustomCategory;
import art.arcane.iris.generation.biome.IrisBiomeCustomParticle;
import art.arcane.iris.generation.biome.IrisBiomeCustomPrecipType;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawn;
import art.arcane.iris.generation.biome.IrisBiomeCustomSpawnType;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionType;
import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
@@ -510,7 +514,7 @@ public class GenerationRegistryContractFactoryTest {
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
semanticFingerprint,
semantic,
registry.generatedDefinitionRendererIdentity(),
"test-renderer-v1",
GenerationRegistryContractFactory.fingerprintDefinition(
generated,
definition,
@@ -528,18 +532,18 @@ public class GenerationRegistryContractFactoryTest {
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(required),
firstFixer,
registry
);
required.requireDefinitionsAvailableIn(available);
assertEquals(
source,
GenerationRegistryContractFactory.requireGeneratedSource(
JsonParser.parseString(source),
JsonParser.parseString(GenerationRegistryContractFactory.renderGeneratedSource(
required,
generated,
firstFixer,
registry
)
firstFixer
))
);
registry.put(generated, PlatformGenerationRegistry.Definition.exactJson(
@@ -547,33 +551,199 @@ public class GenerationRegistryContractFactoryTest {
));
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(Set.of(required), registry)
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(required),
firstFixer,
registry
)
);
registry.remove(generated);
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(Set.of(required), registry)
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(required),
firstFixer,
registry
)
);
}
@Test
public void validatesTheCurrentRenderingWhenTheRendererChangedUnderTheSameIdentity() throws Exception {
FakeGenerationRegistry registry = new FakeGenerationRegistry("runtime-a");
GenerationRegistryContract.PhysicalResourceKey generated = key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"iris:biomes/" + HASH_A
);
IrisBiomeCustom customBiome = new IrisBiomeCustom().setId("mist").setTemperature(0.5D);
String semantic = GenerationRegistryContractFactory.customBiomeEffectiveSemanticJson(customBiome, null);
String capturedSource = customBiome.generateJson(
new TaggedBiomeFixer("first"),
new ContentGate(null, Map.of(), null)
);
GenerationRegistryContract.GeneratedSource generatedSource = new GenerationRegistryContract.GeneratedSource(
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
GenerationRegistryContractFactory.fingerprintCustomBiomeAuthoredDefinition(semantic),
semantic,
"test-renderer-v1",
GenerationRegistryContractFactory.fingerprintDefinition(
generated,
PlatformGenerationRegistry.Definition.exactJson(capturedSource),
"generated"
),
capturedSource
);
GenerationRegistryContract required = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(generated, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(
generated,
generatedSource
)),
Map.of(generated, generatedSource)
);
IDataFixer currentFixer = new TaggedBiomeFixer("second");
String installed = GenerationRegistryContractFactory.renderGeneratedSource(required, generated, currentFixer);
assertNotEquals(JsonParser.parseString(capturedSource), JsonParser.parseString(installed));
registry.put(generated, PlatformGenerationRegistry.Definition.exactJson(installed));
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(required),
currentFixer,
registry
);
FakeGenerationRegistry changedRenderer = new FakeGenerationRegistry("runtime-b") {
@Override
public String generatedDefinitionRendererIdentity() {
return "test-renderer-v2";
}
};
IDataFixer secondFixer = new TaggedBiomeFixer("second");
String rerendered = GenerationRegistryContractFactory.requireGeneratedSource(
required,
generated,
secondFixer,
changedRenderer
required.requireDefinitionsAvailableIn(available);
}
@Test
public void semanticRenderingMatchesTheCapturedRenderingForEverySupportedFixer() throws Exception {
IrisData data = mock(IrisData.class);
when(data.getContentGate()).thenReturn(new ContentGate(null, Map.of(), null));
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
IrisBiomeCustom biome = richCustomBiome();
IrisDimensionTypeOptions options = new IrisDimensionTypeOptions()
.coordinateScale(4.0)
.ambientLight(0.25F)
.fixedTime(18000L)
.cloudHeight(-96)
.monsterSpawnBlockLightLimit(11)
.ultrawarm(IrisDimensionTypeOptions.TriState.TRUE)
.natural(IrisDimensionTypeOptions.TriState.FALSE)
.piglinSafe(IrisDimensionTypeOptions.TriState.TRUE)
.respawnAnchorWorks(IrisDimensionTypeOptions.TriState.TRUE)
.bedWorks(IrisDimensionTypeOptions.TriState.FALSE)
.raids(IrisDimensionTypeOptions.TriState.FALSE)
.skylight(IrisDimensionTypeOptions.TriState.FALSE)
.ceiling(IrisDimensionTypeOptions.TriState.TRUE);
FakeGenerationRegistry registry = new FakeGenerationRegistry("runtime");
GenerationRegistryContract.PhysicalResourceKey dimensionKey = key(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
"iris:overworld_type"
);
assertTrue(rerendered.contains("second"));
assertNotEquals(source, rerendered);
changedRenderer.put(generated, PlatformGenerationRegistry.Definition.exactJson(rerendered));
GenerationRegistryContract availableAfterUpgrade = GenerationRegistryContractFactory
.captureRequiredDefinitions(Set.of(required), secondFixer, changedRenderer);
required.requireDefinitionsAvailableIn(availableAfterUpgrade);
int fixers = 0;
for (DataVersion version : DataVersion.values()) {
if (version == DataVersion.UNSUPPORTED) {
continue;
}
fixers++;
IDataFixer fixer = version.get();
GenerationRegistryContractFactory.CustomBiomeDefinition captured = GenerationRegistryContractFactory
.customBiomeDefinition(data, dimension, biome, fixer, registry);
assertCapturedRenderingRoundTrips(
version,
captured.physicalKey(),
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
captured.semanticFingerprint(),
captured.semanticJson(),
captured.definition(),
fixer,
registry
);
for (IDataFixer.Dimension base : IDataFixer.Dimension.values()) {
IrisDimensionType dimensionType = new IrisDimensionType(base, options, 192, 384, -128);
assertCapturedRenderingRoundTrips(
version,
dimensionKey,
GenerationRegistryContractFactory.DIMENSION_TYPE_EFFECTIVE_SOURCE_SCHEMA,
GenerationEpochContractFactory.fingerprintDimensionType(dimensionType),
GenerationEpochContractFactory.dimensionTypeSemanticJson(dimensionType),
registry.canonicalDefinition(
dimensionKey.registryKey(),
dimensionKey.resourceKey(),
dimensionType.toJson(fixer)
),
fixer,
registry
);
}
}
assertEquals(DataVersion.values().length - 1, fixers);
}
private static void assertCapturedRenderingRoundTrips(
DataVersion version,
GenerationRegistryContract.PhysicalResourceKey key,
String sourceSchema,
String semanticFingerprint,
String semanticJson,
PlatformGenerationRegistry.Definition captured,
IDataFixer fixer,
PlatformGenerationRegistry registry
) throws IOException {
GenerationRegistryContract.GeneratedSource source = new GenerationRegistryContract.GeneratedSource(
sourceSchema,
semanticFingerprint,
semanticJson,
"round-trip",
GenerationRegistryContractFactory.fingerprintDefinition(key, captured, "generated"),
captured.value()
);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, source)),
Map.of(key, source)
);
String rendered = GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer);
assertEquals(
version + " " + key.resourceKey(),
JsonParser.parseString(captured.value()),
JsonParser.parseString(registry.canonicalDefinition(
key.registryKey(),
key.resourceKey(),
rendered
).value())
);
}
private static IrisBiomeCustom richCustomBiome() {
String[] entities = {
"minecraft:zombie", "minecraft:cow", "minecraft:bat", "minecraft:axolotl",
"minecraft:glow_squid", "minecraft:squid", "minecraft:cod", "minecraft:allay"
};
IrisBiomeCustomSpawnType[] groups = IrisBiomeCustomSpawnType.values();
KList<IrisBiomeCustomSpawn> spawns = new KList<>();
for (int index = 0; index < groups.length; index++) {
spawns.add(new IrisBiomeCustomSpawn()
.setType(entities[index % entities.length])
.setGroup(groups[index])
.setWeight(index + 1)
.setMinCount(index + 1)
.setMaxCount(index % 2 == 0 ? index + 3 : index + 1));
}
return new IrisBiomeCustom()
.setId("round_trip")
.setTemperature(1.25D)
.setHumidity(0.3D)
.setDownfallType(IrisBiomeCustomPrecipType.snow)
.setCategory(IrisBiomeCustomCategory.forest)
.setSpawnRarity(7)
.setSkyColor("#112233")
.setFogColor("445566")
.setWaterColor("#778899")
.setWaterFogColor("#aabbcc")
.setGrassColor("#55aa33")
.setFoliageColor("#335511")
.setAmbientParticle(new IrisBiomeCustomParticle().setParticle("ash").setRarity(80))
.setSpawns(spawns);
}
@Test
@@ -621,18 +791,11 @@ public class GenerationRegistryContractFactoryTest {
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, generatedSource)),
Map.of(key, generatedSource)
);
FakeGenerationRegistry upgraded = new FakeGenerationRegistry("runtime-b") {
@Override
public String generatedDefinitionRendererIdentity() {
return "test-renderer-v2";
}
};
String rerendered = GenerationRegistryContractFactory.requireGeneratedSource(
String rerendered = GenerationRegistryContractFactory.renderGeneratedSource(
contract,
key,
new TaggedBiomeFixer("second"),
upgraded
new TaggedBiomeFixer("second")
);
assertTrue(rerendered.contains("second"));
@@ -750,11 +913,6 @@ public class GenerationRegistryContractFactoryTest {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return "test-renderer-v1";
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return "test:biomes/" + identitySha256;
@@ -11,8 +11,6 @@ public interface PlatformGenerationRegistry extends NativeGenerationRegistry {
String runtimeIdentity();
String generatedDefinitionRendererIdentity();
default String customBiomeResourceKey(String identitySha256) {
return contentAddressedCustomBiomeResourceKey(identitySha256);
}