mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
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:
@@ -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("===============================================================");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user