Fix biome names and conditions

- Register readable client biome IDs while preserving saved registry identities.
- Audit the modded registry naming mixin.
- Return false from MythicMobs conditions while saved biome data loads.
- Cover biome naming and MythicMobs loading behavior with regression tests.
This commit is contained in:
Brian Neumann-Fopiano
2026-10-04 20:10:26 -04:00
parent ee5aaec87c
commit c479e94c49
6 changed files with 261 additions and 10 deletions
@@ -45,6 +45,9 @@ public final class ModdedMixinAudit {
private static final AtomicBoolean AUDITED = new AtomicBoolean(false);
static final List<ExpectedMixin> EXPECTED = List.of(
new ExpectedMixin(ENTITY_CONFIG, "RegistryClientNamesMixin",
NativeMixinTarget.REGISTRY_PACKET, "volmlib$clientRegistryNames",
false, NativeMixinFlags::registryClientNamesRan),
new ExpectedMixin(ENTITY_CONFIG, "EntityPersistenceMixin",
NativeMixinTarget.ENTITY, "iris$applyGeneratedPersistence",
false, NativeMixinFlags::entityPersistenceRan),
@@ -108,7 +111,7 @@ public final class ModdedMixinAudit {
ModdedIrisLog.error(" missing: {}", entry);
}
ModdedIrisLog.error("The mixin config was not registered for this loader (fabric.mod.json mixins, neoforge.mods.toml [[mixins]], forge MixinConfigs manifest attribute).");
ModdedIrisLog.error("Entity persistence, custom mob loot, parallel structure safety, or Iris world-type labels are disabled until this is fixed.");
ModdedIrisLog.error("Entity persistence, custom mob loot, parallel structure safety, readable biome identifiers, or Iris world-type labels are disabled until this is fixed.");
ModdedIrisLog.error("===============================================================");
}
+2
View File
@@ -168,6 +168,8 @@ dependencies {
testImplementation(libs.log4j.core)
testRuntimeOnly(libs.paper.api)
testImplementation(libs.multiverseCore)
testImplementation(libs.annotations)
testImplementation(libs.mythic)
}
tasks.named('test').configure {
@@ -3,6 +3,7 @@ package art.arcane.iris.integration.data;
import art.arcane.iris.integration.ExternalDataProvider;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
@@ -110,10 +111,17 @@ public class MythicMobsDataProvider extends ExternalDataProvider implements List
if (access == null) return false;
Engine engine = access.getEngine();
if (engine == null) return false;
IrisBiome biome = surface ?
engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) :
engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
return biomes.contains(biome.getLoadKey());
try {
IrisBiome biome = surface ?
engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) :
engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
return biomes.contains(biome.getLoadKey());
} catch (SavedBiomeUnavailableException unavailable) {
if (unavailable.isLoading() && unavailable.getSuppressed().length == 0) {
return false;
}
throw unavailable;
}
}
}
@@ -134,9 +142,16 @@ public class MythicMobsDataProvider extends ExternalDataProvider implements List
if (access == null) return false;
Engine engine = access.getEngine();
if (engine == null) return false;
IrisRegion region = engine.getRegion(
target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
return regions.contains(region.getLoadKey());
try {
IrisRegion region = engine.getRegion(
target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
return regions.contains(region.getLoadKey());
} catch (SavedBiomeUnavailableException unavailable) {
if (unavailable.isLoading() && unavailable.getSuppressed().length == 0) {
return false;
}
throw unavailable;
}
}
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.volmlib.nativelib.terrain.RegistryClientNames;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonArray;
@@ -253,6 +254,7 @@ public final class GenerationRegistryContractFactory {
requiredBiome.getId()
);
String resourceKey = requiredRegistry.customBiomeResourceKey(identityFingerprint);
registerCustomBiomeName(requiredDimension.getLoadKey(), requiredBiome.getId(), resourceKey);
GenerationRegistryContract.PhysicalResourceKey physicalKey = key(BIOME_REGISTRY, resourceKey);
PlatformGenerationRegistry.Definition canonicalDefinition = requiredRegistry.canonicalDefinition(
BIOME_REGISTRY,
@@ -285,13 +287,15 @@ public final class GenerationRegistryContractFactory {
ContentGate contentGate
) {
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(
String resourceKey = PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(
fingerprintCustomBiomeIdentity(
fingerprintCustomBiomeEffectiveDefinition(requiredBiome, contentGate),
dimensionKey,
requiredBiome.getId()
)
);
registerCustomBiomeName(dimensionKey, requiredBiome.getId(), resourceKey);
return resourceKey;
}
public static String customBiomeResourceKey(
@@ -310,13 +314,15 @@ public final class GenerationRegistryContractFactory {
) {
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
PlatformGenerationRegistry requiredRegistry = Objects.requireNonNull(registry, "registry");
return requiredRegistry.customBiomeResourceKey(
String resourceKey = requiredRegistry.customBiomeResourceKey(
fingerprintCustomBiomeIdentity(
fingerprintCustomBiomeEffectiveDefinition(requiredBiome, contentGate),
dimensionKey,
requiredBiome.getId()
)
);
registerCustomBiomeName(dimensionKey, requiredBiome.getId(), resourceKey);
return resourceKey;
}
public static String fingerprintCustomBiomeIdentity(
@@ -387,6 +393,7 @@ public final class GenerationRegistryContractFactory {
}
}
if (selectedKey != null) {
registerCustomBiomeName(dimensionKey, customBiomeId, selectedKey);
return selectedKey;
}
throw new IOException("Historical generation registry contract has no custom biome mapping for "
@@ -1539,6 +1546,14 @@ public final class GenerationRegistryContractFactory {
}
}
private static void registerCustomBiomeName(String dimensionKey, String biomeId, String resourceKey) {
String dimension = requireText(dimensionKey, "Dimension load key").toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9/._-]", "_");
String biome = requireText(biomeId, "Custom biome ID").toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9/._-]", "_");
RegistryClientNames.register(BIOME_REGISTRY, resourceKey, "iris:" + dimension + "/" + biome);
}
private static void updateString(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
updateInt(digest, bytes.length);
@@ -1608,6 +1623,7 @@ public final class GenerationRegistryContractFactory {
+ dimension + ":" + requiredBiome.getId() + ".");
}
requireGeneratedSemanticFingerprint(physicalKey, source, contract.definitions().get(physicalKey));
registerCustomBiomeName(dimension, requiredBiome.getId(), resourceKey);
dimensionKeys.put(requiredBiome, resourceKey);
return resourceKey;
}
@@ -0,0 +1,192 @@
package art.arcane.iris.integration.data;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import io.lumine.mythic.api.adapters.AbstractLocation;
import io.lumine.mythic.api.config.MythicLineConfig;
import io.lumine.mythic.api.skills.conditions.ILocationCondition;
import io.lumine.mythic.bukkit.MythicBukkit;
import io.lumine.mythic.bukkit.adapters.BukkitWorld;
import io.lumine.mythic.core.skills.SkillExecutor;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MythicMobsDataProviderTest {
@Test
public void loadingChecksRetryTheSameLocationOnceSavedEnvironmentIsReady() {
for (Query query : Query.values()) {
try (Fixture fixture = new Fixture()) {
ILocationCondition condition = fixture.condition(query);
fixture.failure = new SavedBiomeUnavailableException("Saved biome information is loading", true);
assertFalse(condition.check(fixture.target));
fixture.failure = null;
assertTrue(condition.check(fixture.target));
}
}
}
@Test
public void readyEnvironmentMustMatchTheConfiguredName() {
for (Query query : Query.values()) {
try (Fixture fixture = new Fixture()) {
ILocationCondition condition = fixture.condition(query);
when(fixture.biome.getLoadKey()).thenReturn("other");
when(fixture.region.getLoadKey()).thenReturn("other");
assertFalse(condition.check(fixture.target));
}
}
}
@Test
public void locationsWithoutAnIrisGeneratorOrEngineDoNotMatch() {
for (Query query : Query.values()) {
try (Fixture fixture = new Fixture()) {
ILocationCondition condition = fixture.condition(query);
fixture.toolbelt.when(() -> IrisToolbelt.access(fixture.world)).thenReturn(null);
assertFalse(condition.check(fixture.target));
fixture.toolbelt.when(() -> IrisToolbelt.access(fixture.world)).thenReturn(fixture.generator);
when(fixture.generator.getEngine()).thenReturn(null);
assertFalse(condition.check(fixture.target));
}
}
}
@Test
public void permanentSavedEnvironmentFailuresPropagateUnchanged() {
for (Query query : Query.values()) {
try (Fixture fixture = new Fixture()) {
ILocationCondition condition = fixture.condition(query);
fixture.failure = new SavedBiomeUnavailableException("Saved epoch is missing", false);
assertSame(fixture.failure, assertThrows(SavedBiomeUnavailableException.class,
() -> condition.check(fixture.target)));
}
}
}
@Test
public void loadingFailuresWithSuppressedErrorsRemainVisible() {
for (Query query : Query.values()) {
try (Fixture fixture = new Fixture()) {
ILocationCondition condition = fixture.condition(query);
fixture.failure = new SavedBiomeUnavailableException("Saved biome information is loading", true);
fixture.failure.addSuppressed(new IllegalStateException("failed to close saved epoch"));
assertSame(fixture.failure, assertThrows(SavedBiomeUnavailableException.class,
() -> condition.check(fixture.target)));
}
}
}
@Test
public void unrelatedRuntimeFailuresAreNotHidden() {
for (Query query : Query.values()) {
try (Fixture fixture = new Fixture()) {
ILocationCondition condition = fixture.condition(query);
fixture.failure = new IllegalStateException("broken integration query");
assertSame(fixture.failure, assertThrows(IllegalStateException.class,
() -> condition.check(fixture.target)));
}
}
}
@Test
public void volumeAndRegionQueriesUseWorldHeightWhileSurfaceUsesHorizontalCoordinates() {
try (Fixture fixture = new Fixture()) {
assertTrue(fixture.condition(Query.VOLUME).check(fixture.target));
assertTrue(fixture.condition(Query.SURFACE).check(fixture.target));
assertTrue(fixture.condition(Query.REGION).check(fixture.target));
verify(fixture.engine).getBiomeOrMantle(12, 134, -18);
verify(fixture.engine).getSurfaceBiome(12, -18);
verify(fixture.engine).getRegion(12, 134, -18);
}
}
private enum Query {
VOLUME, SURFACE, REGION
}
private static final class Fixture implements AutoCloseable {
private final World world = mock(World.class);
private final PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
private final Engine engine = mock(Engine.class);
private final IrisBiome biome = mock(IrisBiome.class);
private final IrisRegion region = mock(IrisRegion.class);
private final AbstractLocation target = mock(AbstractLocation.class);
private final MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
private final MockedStatic<MythicBukkit> mythic = mockStatic(MythicBukkit.class);
private RuntimeException failure;
private Fixture() {
MythicBukkit plugin = mock(MythicBukkit.class);
when(plugin.getSkillManager()).thenReturn(mock(SkillExecutor.class));
mythic.when(MythicBukkit::inst).thenReturn(plugin);
BukkitWorld targetWorld = mock(BukkitWorld.class);
when(targetWorld.getBukkitWorld()).thenReturn(world);
when(target.getWorld()).thenReturn(targetWorld);
when(target.getBlockX()).thenReturn(12);
when(target.getBlockY()).thenReturn(70);
when(target.getBlockZ()).thenReturn(-18);
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
when(generator.getEngine()).thenReturn(engine);
when(engine.getMinHeight()).thenReturn(-64);
when(biome.getLoadKey()).thenReturn("matching");
when(region.getLoadKey()).thenReturn("matching");
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> resolvedBiome());
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> resolvedBiome());
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> resolvedRegion());
}
private ILocationCondition condition(Query query) {
MythicLineConfig config = mock(MythicLineConfig.class);
when(config.getString(any(String[].class), eq(""))).thenReturn("matching");
when(config.getBoolean(any(String[].class), eq(false))).thenReturn(query == Query.SURFACE);
return query == Query.REGION
? new MythicMobsDataProvider.IrisRegionCondition("irisregion", config)
: new MythicMobsDataProvider.IrisBiomeCondition("irisbiome", config);
}
private IrisBiome resolvedBiome() {
if (failure != null) {
throw failure;
}
return biome;
}
private IrisRegion resolvedRegion() {
if (failure != null) {
throw failure;
}
return region;
}
@Override
public void close() {
mythic.close();
toolbelt.close();
}
}
}
@@ -19,6 +19,7 @@ import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
import art.arcane.iris.structure.placement.IrisStructurePlacement;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.volmlib.nativelib.terrain.RegistryClientNames;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
@@ -195,6 +196,9 @@ public class GenerationRegistryContractFactoryTest {
new GenerationRegistryContractFactory.CustomBiomeResourceResolver(contract, registry::customBiomeResourceKey);
assertEquals(plateauKey, resolver.resolve("overworld", plateau));
assertEquals(vanillaKey, resolver.resolve("overworld", vanilla));
assertEquals(Set.of("iris:overworld/savanna_plateau", "iris_2:overworld/savanna_plateau"),
Set.copyOf(RegistryClientNames.resolve(GenerationRegistryContractFactory.BIOME_REGISTRY,
List.of(plateauKey, vanillaKey))));
assertNotEquals(plateauKey, GenerationRegistryContractFactory.customBiomeResourceKey("overworld", plateau, registry));
assertThrows(IOException.class,
() -> resolver.resolve("overworld", new IrisBiomeCustom().setId("savanna_plateau").setGrassColor("#FF0000")));
@@ -236,6 +240,25 @@ public class GenerationRegistryContractFactoryTest {
PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(first));
}
@Test
public void clientBiomeNamesUseAuthoredIdsWhilePhysicalIdentitiesStayDistinct() {
IrisBiomeCustom first = new IrisBiomeCustom().setId("Mist").setTemperature(0.5D);
IrisBiomeCustom second = new IrisBiomeCustom().setId("Mist").setTemperature(0.7D);
String firstKey = GenerationRegistryContractFactory.customBiomeResourceKey("Overworld", first);
String secondKey = GenerationRegistryContractFactory.customBiomeResourceKey("Overworld", second);
List<String> physicalKeys = List.of("minecraft:plains", firstKey, secondKey);
List<String> clientKeys = RegistryClientNames.resolve(
GenerationRegistryContractFactory.BIOME_REGISTRY, physicalKeys);
assertNotEquals(firstKey, secondKey);
assertEquals("minecraft:plains", clientKeys.getFirst());
assertEquals(Set.of("iris:overworld/mist", "iris_2:overworld/mist"),
Set.of(clientKeys.get(1), clientKeys.get(2)));
assertTrue(firstKey.matches("iris:biomes/[a-f0-9]{64}"));
assertEquals(List.of("minecraft:plains", firstKey, secondKey), physicalKeys);
}
@Test
public void contentGateEffectsParticipateInCustomBiomeIdentity() {
IrisBiomeCustom custom = new IrisBiomeCustom()