Adapter and papi fixes

This commit is contained in:
Brian Neumann-Fopiano
2026-09-13 21:27:44 -04:00
parent c14a972ab8
commit dfb3748059
15 changed files with 796 additions and 30 deletions
@@ -0,0 +1,24 @@
package art.arcane.iris.api.terrain;
import java.util.List;
public record IrisBiomeInfo(
String key,
String name,
String regionKey,
String regionName,
String derivativeKey,
String vanillaDerivativeKey,
String type,
List<IrisCustomBiomeInfo> customDerivatives) {
public IrisBiomeInfo {
key = key == null ? "" : key;
name = name == null ? "" : name;
regionKey = regionKey == null ? "" : regionKey;
regionName = regionName == null ? "" : regionName;
derivativeKey = derivativeKey == null ? "" : derivativeKey;
vanillaDerivativeKey = vanillaDerivativeKey == null ? "" : vanillaDerivativeKey;
type = type == null ? "" : type;
customDerivatives = List.copyOf(customDerivatives);
}
}
@@ -0,0 +1,8 @@
package art.arcane.iris.api.terrain;
public record IrisCustomBiomeInfo(String id, String registryKey) {
public IrisCustomBiomeInfo {
id = id == null ? "" : id;
registryKey = registryKey == null ? "" : registryKey;
}
}
@@ -18,6 +18,8 @@ public interface IrisTerrainService {
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<IrisBiomeInfo> surfaceBiomeInfo(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
@@ -42,6 +42,19 @@ public final class IrisPapiExpansion extends VolmitPlaceholderExpansion {
.key("world.available", state::worldAvailable)
.key("world.biome", state::biome)
.key("world.biome-key", state::biomeKey)
.key("world.biome-custom", state::biomeCustom)
.key("world.biome-custom-id", state::biomeCustomId)
.key("world.biome-custom-ids", state::biomeCustomIds)
.key("world.biome-custom-key", state::biomeCustomKey)
.key("world.biome-custom-keys", state::biomeCustomKeys)
.key("world.biome-custom-count", state::biomeCustomCount)
.key("world.biome-derivative", state::biomeDerivative)
.key("world.biome-vanilla-derivative", state::biomeVanillaDerivative)
.key("world.biome-type", state::biomeType)
.key("world.min-height", state::minHeight)
.key("world.max-height", state::maxHeight)
.key("world.height", state::height)
.key("world.fluid-height", state::fluidHeight)
.key("world.region", state::region)
.key("world.region-key", state::regionKey)
.key("world.dimension", state::dimension)
@@ -3,7 +3,6 @@ package art.arcane.iris.integration;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderSnapshot;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import art.arcane.volmlib.util.bukkit.papi.PlayerSnapshotStore;
@@ -103,6 +102,71 @@ public final class IrisPapiState {
return view == null ? PlaceholderValues.UNAVAILABLE : view.biomeKey();
}
public String biomeCustom(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().custom();
}
public String biomeCustomId(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().customId();
}
public String biomeCustomIds(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().customIds();
}
public String biomeCustomKey(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().customKey();
}
public String biomeCustomKeys(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().customKeys();
}
public String biomeCustomCount(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().customCount();
}
public String biomeDerivative(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().derivative();
}
public String biomeVanillaDerivative(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().vanillaDerivative();
}
public String biomeType(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.details().type();
}
public String minHeight(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.heights().minimum();
}
public String maxHeight(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.heights().maximum();
}
public String height(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.heights().height();
}
public String fluidHeight(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.heights().fluid();
}
public String region(UUID playerId) {
IrisPapiWorldView view = viewOf(playerId);
return view == null ? PlaceholderValues.UNAVAILABLE : view.region();
@@ -195,10 +259,7 @@ public final class IrisPapiState {
return IrisPapiWorldView.present(
position,
now,
service.surfaceBiomeName(world, blockX, blockZ),
service.surfaceBiomeKey(world, blockX, blockZ),
service.regionName(world, blockX, blockZ),
service.regionKey(world, blockX, blockZ),
service.worldInfo(world).map(IrisWorldInfo::dimensionKey));
service.surfaceBiomeInfo(world, blockX, blockZ),
service.worldInfo(world));
}
}
@@ -1,8 +1,13 @@
package art.arcane.iris.integration;
import art.arcane.iris.api.terrain.IrisBiomeInfo;
import art.arcane.iris.api.terrain.IrisCustomBiomeInfo;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;
public record IrisPapiWorldView(
IrisPapiPosition position,
@@ -12,7 +17,9 @@ public record IrisPapiWorldView(
String biomeKey,
String region,
String regionKey,
String dimension) {
String dimension,
BiomeDetails details,
Heights heights) {
public static IrisPapiWorldView absent(IrisPapiPosition position, long builtAtMs) {
return new IrisPapiWorldView(
position,
@@ -22,29 +29,82 @@ public record IrisPapiWorldView(
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE,
PlaceholderValues.UNAVAILABLE);
PlaceholderValues.UNAVAILABLE,
BiomeDetails.absent(),
Heights.absent());
}
public static IrisPapiWorldView present(
IrisPapiPosition position,
long builtAtMs,
Optional<String> biome,
Optional<String> biomeKey,
Optional<String> region,
Optional<String> regionKey,
Optional<String> dimension) {
Optional<IrisBiomeInfo> biome,
Optional<IrisWorldInfo> world) {
return new IrisPapiWorldView(
position,
builtAtMs,
PlaceholderValues.TRUE,
text(biome),
text(biomeKey),
text(region),
text(regionKey),
text(dimension));
text(biome.map(IrisBiomeInfo::name)),
text(biome.map(IrisBiomeInfo::key)),
text(biome.map(IrisBiomeInfo::regionName)),
text(biome.map(IrisBiomeInfo::regionKey)),
text(world.map(IrisWorldInfo::dimensionKey)),
biome.map(BiomeDetails::of).orElseGet(BiomeDetails::absent),
world.map(Heights::of).orElseGet(Heights::absent));
}
static String text(Optional<String> value) {
return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get());
}
public record BiomeDetails(
String custom,
String customId,
String customIds,
String customKey,
String customKeys,
String customCount,
String derivative,
String vanillaDerivative,
String type) {
static BiomeDetails absent() {
String unavailable = PlaceholderValues.UNAVAILABLE;
return new BiomeDetails(unavailable, unavailable, unavailable, unavailable, unavailable,
unavailable, unavailable, unavailable, unavailable);
}
static BiomeDetails of(IrisBiomeInfo biome) {
List<IrisCustomBiomeInfo> derivatives = biome.customDerivatives();
StringJoiner ids = new StringJoiner(", ");
StringJoiner keys = new StringJoiner(", ");
boolean keysComplete = true;
for (IrisCustomBiomeInfo derivative : derivatives) {
ids.add(derivative.id());
keys.add(derivative.registryKey());
keysComplete &= !derivative.registryKey().isBlank();
}
IrisCustomBiomeInfo single = derivatives.size() == 1 ? derivatives.getFirst() : null;
return new BiomeDetails(
PlaceholderValues.bool(!derivatives.isEmpty()),
single == null ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(single.id()),
PlaceholderValues.text(ids.toString()),
single == null ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(single.registryKey()),
keysComplete ? PlaceholderValues.text(keys.toString()) : PlaceholderValues.UNAVAILABLE,
Integer.toString(derivatives.size()),
PlaceholderValues.text(biome.derivativeKey()),
PlaceholderValues.text(biome.vanillaDerivativeKey()),
PlaceholderValues.text(biome.type()));
}
}
public record Heights(String minimum, String maximum, String height, String fluid) {
static Heights absent() {
String unavailable = PlaceholderValues.UNAVAILABLE;
return new Heights(unavailable, unavailable, unavailable, unavailable);
}
static Heights of(IrisWorldInfo world) {
return new Heights(Integer.toString(world.minHeight()), Integer.toString(world.maxHeight()),
Integer.toString(world.height()), Integer.toString(world.fluidHeight()));
}
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.platform.bukkit.api;
import art.arcane.iris.api.terrain.IrisBiomeInfo;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSample;
@@ -10,6 +11,7 @@ import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.platform.bukkit.terrain.IrisApiFaultGuard;
import art.arcane.iris.platform.bukkit.terrain.IrisBiomeInfoFactory;
import art.arcane.iris.platform.bukkit.terrain.IrisColumnWalk;
import art.arcane.iris.platform.bukkit.terrain.IrisSampleLimits;
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
@@ -25,6 +27,7 @@ import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
@@ -150,6 +153,22 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
}
}
@Override
public Optional<IrisBiomeInfo> surfaceBiomeInfo(World world, int blockX, int blockZ) {
Engine engine = liveEngineOf(world);
if (engine == null) {
return Optional.empty();
}
try {
return Optional.of(IrisBiomeInfoFactory.surface(engine, blockX, blockZ,
error -> reportQueryFault("surfaceBiomeInfo", world, error)));
} catch (Throwable error) {
reportQueryFault("surfaceBiomeInfo", world, error);
return Optional.empty();
}
}
@Override
public Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ) {
Engine engine = liveEngineOf(world);
@@ -371,6 +390,10 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
}
private void reportQueryFault(String operation, World world, Throwable error) {
if (error instanceof SavedBiomeUnavailableException unavailable
&& unavailable.isLoading() && unavailable.getSuppressed().length == 0) {
return;
}
if (queryFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API query \"" + operation + "\" failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + queryFaults.faults()
@@ -379,6 +402,10 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
}
private void reportSinkFault(World world, Throwable error) {
if (error instanceof SavedBiomeUnavailableException unavailable
&& unavailable.isLoading() && unavailable.getSuppressed().length == 0) {
return;
}
if (sinkFaults.record(System.currentTimeMillis())) {
IrisLogging.reportError("Iris terrain API column sample failed for world \""
+ (world == null ? "null" : world.getName()) + "\" (" + sinkFaults.faults()
@@ -0,0 +1,59 @@
package art.arcane.iris.platform.bukkit.terrain;
import art.arcane.iris.api.terrain.IrisBiomeInfo;
import art.arcane.iris.api.terrain.IrisCustomBiomeInfo;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.volmlib.util.collection.KList;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
public final class IrisBiomeInfoFactory {
private IrisBiomeInfoFactory() {
}
public static IrisBiomeInfo surface(Engine engine, int blockX, int blockZ, Consumer<Throwable> reportFailure) {
Objects.requireNonNull(reportFailure, "reportFailure");
BiomeEnvironment environment = engine.getSurfaceBiomeEnvironment(blockX, blockZ);
IrisBiome biome = environment.biome();
InferredType inferredType = biome.getInferredType();
return new IrisBiomeInfo(
biome.getLoadKey(),
biome.getName(),
environment.region().getLoadKey(),
environment.region().getName(),
biome.getDerivativeKey(),
biome.getVanillaDerivativeKey(),
inferredType == null ? "" : inferredType.name().toLowerCase(Locale.ROOT),
customDerivatives(environment, reportFailure));
}
private static List<IrisCustomBiomeInfo> customDerivatives(
BiomeEnvironment environment,
Consumer<Throwable> reportFailure) {
KList<IrisBiomeCustom> customBiomes = environment.biome().getCustomDerivitives();
if (customBiomes == null || customBiomes.isEmpty()) {
return List.of();
}
List<IrisCustomBiomeInfo> derivatives = new ArrayList<>(customBiomes.size());
for (IrisBiomeCustom customBiome : customBiomes) {
String registryKey = "";
try {
registryKey = environment.data().customBiomeResourceKey(environment.dimension(), customBiome);
} catch (RuntimeException failure) {
reportFailure.accept(new IllegalStateException(
"Unable to resolve the registry key for Iris custom biome '" + customBiome.getId() + "'.", failure));
}
derivatives.add(new IrisCustomBiomeInfo(customBiome.getId(), registryKey));
}
return derivatives;
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.api.terrain;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class IrisBiomeInfoTest {
@Test
public void snapshotsDoNotExposeMutableDerivativeLists() {
List<IrisCustomBiomeInfo> derivatives = new ArrayList<>();
derivatives.add(new IrisCustomBiomeInfo("amber", "iris:biomes/amber"));
IrisBiomeInfo info = new IrisBiomeInfo(
"forests/amber", "Amber Forest", "temperate", "Temperate",
"minecraft:forest", "minecraft:forest", "land", derivatives);
derivatives.clear();
assertEquals(1, info.customDerivatives().size());
assertThrows(UnsupportedOperationException.class, () -> info.customDerivatives().clear());
}
@Test
public void unavailableStringsAreEmpty() {
IrisBiomeInfo info = new IrisBiomeInfo(null, null, null, null, null, null, null, List.of());
IrisCustomBiomeInfo custom = new IrisCustomBiomeInfo(null, null);
assertEquals("", info.key());
assertEquals("", info.name());
assertEquals("", info.regionKey());
assertEquals("", info.regionName());
assertEquals("", info.derivativeKey());
assertEquals("", info.vanillaDerivativeKey());
assertEquals("", info.type());
assertEquals("", custom.id());
assertEquals("", custom.registryKey());
}
}
@@ -1,5 +1,7 @@
package art.arcane.iris.integration;
import art.arcane.iris.api.terrain.IrisBiomeInfo;
import art.arcane.iris.api.terrain.IrisCustomBiomeInfo;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
@@ -8,6 +10,7 @@ import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.World;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
@@ -20,6 +23,8 @@ final class FakeIrisTerrainService implements IrisTerrainService {
private String biomeKey = "desert/hot-dunes";
private String regionName = "Scorched Expanse";
private String regionKey = "scorched";
private List<IrisCustomBiomeInfo> derivatives = List.of(new IrisCustomBiomeInfo("golden-dunes", "iris:biomes/" + "a".repeat(64)));
private boolean biomeAvailable = true;
private int builds;
private int lastBlockX;
private int lastBlockZ;
@@ -36,6 +41,26 @@ final class FakeIrisTerrainService implements IrisTerrainService {
this.regionKey = regionKey;
}
void derivatives(List<IrisCustomBiomeInfo> derivatives) {
this.derivatives = derivatives;
}
void biomeAvailable(boolean available) {
biomeAvailable = available;
}
@Override
public Optional<IrisBiomeInfo> surfaceBiomeInfo(World world, int blockX, int blockZ) {
if (!isIrisWorld(world) || !biomeAvailable) {
return Optional.empty();
}
builds++;
lastBlockX = blockX;
lastBlockZ = blockZ;
return Optional.of(new IrisBiomeInfo(biomeKey, biomeName, regionKey, regionName,
"minecraft:desert", "minecraft:desert", "land", derivatives));
}
int builds() {
return builds;
}
@@ -70,19 +95,12 @@ final class FakeIrisTerrainService implements IrisTerrainService {
@Override
public Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ) {
return isIrisWorld(world) ? Optional.ofNullable(biomeKey) : Optional.empty();
throw new AssertionError("placeholders must use a single biome metadata snapshot");
}
@Override
public Optional<String> surfaceBiomeName(World world, int blockX, int blockZ) {
if (!isIrisWorld(world)) {
return Optional.empty();
}
builds++;
lastBlockX = blockX;
lastBlockZ = blockZ;
return Optional.ofNullable(biomeName);
throw new AssertionError("placeholders must use a single biome metadata snapshot");
}
@Override
@@ -92,12 +110,12 @@ final class FakeIrisTerrainService implements IrisTerrainService {
@Override
public Optional<String> regionKey(World world, int blockX, int blockZ) {
return isIrisWorld(world) ? Optional.ofNullable(regionKey) : Optional.empty();
throw new AssertionError("placeholders must use a single biome metadata snapshot");
}
@Override
public Optional<String> regionName(World world, int blockX, int blockZ) {
return isIrisWorld(world) ? Optional.ofNullable(regionName) : Optional.empty();
throw new AssertionError("placeholders must use a single biome metadata snapshot");
}
@Override
@@ -36,8 +36,21 @@ public class IrisPapiExpansionTest {
"pregen.world",
"world.available",
"world.biome",
"world.biome-custom",
"world.biome-custom-count",
"world.biome-custom-id",
"world.biome-custom-ids",
"world.biome-custom-key",
"world.biome-custom-keys",
"world.biome-derivative",
"world.biome-key",
"world.biome-type",
"world.biome-vanilla-derivative",
"world.dimension",
"world.fluid-height",
"world.height",
"world.max-height",
"world.min-height",
"world.region",
"world.region-key");
@@ -102,6 +115,19 @@ public class IrisPapiExpansionTest {
assertEquals("true", expansion.onRequest(player, "world.available"));
assertEquals("Hot Desert Dunes", expansion.onRequest(player, "world.biome"));
assertEquals("desert/hot-dunes", expansion.onRequest(player, "world.biome-key"));
assertEquals("true", expansion.onRequest(player, "world.biome-custom"));
assertEquals("golden-dunes", expansion.onRequest(player, "world.biome-custom-id"));
assertEquals("golden-dunes", expansion.onRequest(player, "world.biome-custom-ids"));
assertEquals("iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expansion.onRequest(player, "world.biome-custom-key"));
assertEquals("iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", expansion.onRequest(player, "world.biome-custom-keys"));
assertEquals("1", expansion.onRequest(player, "world.biome-custom-count"));
assertEquals("minecraft:desert", expansion.onRequest(player, "world.biome-derivative"));
assertEquals("minecraft:desert", expansion.onRequest(player, "world.biome-vanilla-derivative"));
assertEquals("land", expansion.onRequest(player, "world.biome-type"));
assertEquals("-64", expansion.onRequest(player, "world.min-height"));
assertEquals("320", expansion.onRequest(player, "world.max-height"));
assertEquals("384", expansion.onRequest(player, "world.height"));
assertEquals("63", expansion.onRequest(player, "world.fluid-height"));
assertEquals("Scorched Expanse", expansion.onRequest(player, "world.region"));
assertEquals("scorched", expansion.onRequest(player, "world.region-key"));
assertEquals("overworld", expansion.onRequest(player, "world.dimension"));
@@ -1,5 +1,6 @@
package art.arcane.iris.integration;
import art.arcane.iris.api.terrain.IrisCustomBiomeInfo;
import art.arcane.iris.api.pregen.IrisPregenPhase;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.terrain.IrisTerrainService;
@@ -110,6 +111,19 @@ public class IrisPapiStateTest {
state.region(PLAYER);
state.regionKey(PLAYER);
state.dimension(PLAYER);
state.biomeCustom(PLAYER);
state.biomeCustomId(PLAYER);
state.biomeCustomIds(PLAYER);
state.biomeCustomKey(PLAYER);
state.biomeCustomKeys(PLAYER);
state.biomeCustomCount(PLAYER);
state.biomeDerivative(PLAYER);
state.biomeVanillaDerivative(PLAYER);
state.biomeType(PLAYER);
state.minHeight(PLAYER);
state.maxHeight(PLAYER);
state.height(PLAYER);
state.fluidHeight(PLAYER);
}
assertEquals(1, terrain.builds());
@@ -391,4 +405,117 @@ public class IrisPapiStateTest {
assertEquals("false", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biome(PLAYER));
}
@Test
public void multipleCustomDerivativesExposeAllDefinitionsWithoutGuessingTheSelectedOne() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
terrain.derivatives(List.of(
new IrisCustomBiomeInfo("golden-dunes", "iris:biomes/first"),
new IrisCustomBiomeInfo("red-dunes", "iris:biomes/second")));
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("true", state.biomeCustom(PLAYER));
assertEquals("2", state.biomeCustomCount(PLAYER));
assertEquals("golden-dunes, red-dunes", state.biomeCustomIds(PLAYER));
assertEquals("iris:biomes/first, iris:biomes/second", state.biomeCustomKeys(PLAYER));
assertEquals(DASH, state.biomeCustomId(PLAYER));
assertEquals(DASH, state.biomeCustomKey(PLAYER));
}
@Test
public void vanillaBiomesExposeFalseAndARealZeroCustomCount() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
terrain.derivatives(List.of());
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("false", state.biomeCustom(PLAYER));
assertEquals("0", state.biomeCustomCount(PLAYER));
assertEquals(DASH, state.biomeCustomId(PLAYER));
assertEquals(DASH, state.biomeCustomIds(PLAYER));
assertEquals(DASH, state.biomeCustomKeys(PLAYER));
assertEquals("minecraft:desert", state.biomeVanillaDerivative(PLAYER));
}
@Test
public void anUnavailableRegistryKeyPreservesAuthoredIdsWithoutPublishingPartialKeyLists() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
terrain.derivatives(List.of(new IrisCustomBiomeInfo("golden-dunes", "")));
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("golden-dunes", state.biomeCustomId(PLAYER));
assertEquals(DASH, state.biomeCustomKey(PLAYER));
terrain.derivatives(List.of(new IrisCustomBiomeInfo("golden-dunes", "iris:biomes/first"),
new IrisCustomBiomeInfo("red-dunes", "")));
clock.advance(IrisPapiState.VIEW_TTL_MS);
assertEquals("golden-dunes, red-dunes", state.biomeCustomIds(PLAYER));
assertEquals(DASH, state.biomeCustomKeys(PLAYER));
}
@Test
public void missingBiomeMetadataDoesNotHideKnownDimensionHeights() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
terrain.biomeAvailable(false);
IrisPapiState state = new IrisPapiState(() -> terrain, new IrisPapiTestSupport.Clock());
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("true", state.worldAvailable(PLAYER));
assertEquals(DASH, state.biomeCustom(PLAYER));
assertEquals(DASH, state.biomeCustomCount(PLAYER));
assertEquals(DASH, state.biomeDerivative(PLAYER));
assertEquals("overworld", state.dimension(PLAYER));
assertEquals("-64", state.minHeight(PLAYER));
assertEquals("320", state.maxHeight(PLAYER));
assertEquals("384", state.height(PLAYER));
assertEquals("63", state.fluidHeight(PLAYER));
}
@Test
public void pendingBiomeMetadataIsRetriedAfterTheViewExpiresWithoutPlayerMovement() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
terrain.biomeAvailable(false);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals(DASH, state.biomeCustomId(PLAYER));
assertEquals("true", state.worldAvailable(PLAYER));
terrain.biomeAvailable(true);
clock.advance(IrisPapiState.VIEW_TTL_MS - 1L);
assertEquals(DASH, state.biomeCustomId(PLAYER));
clock.advance(1L);
assertEquals("golden-dunes", state.biomeCustomId(PLAYER));
assertEquals("Hot Desert Dunes", state.biome(PLAYER));
}
@Test
public void customDerivativeIdsAreScrubbedAndRefreshWithTheCachedView() {
FakeIrisTerrainService terrain = new FakeIrisTerrainService();
World world = IrisPapiTestSupport.world("sandbox");
terrain.addIrisWorld(world);
IrisPapiTestSupport.Clock clock = new IrisPapiTestSupport.Clock();
IrisPapiState state = new IrisPapiState(() -> terrain, clock);
state.trackPosition(PLAYER, world, 10, 10);
assertEquals("golden-dunes", state.biomeCustomId(PLAYER));
terrain.derivatives(List.of(new IrisCustomBiomeInfo("§6red%dunes", "iris:biomes/second")));
assertEquals("golden-dunes", state.biomeCustomIds(PLAYER));
clock.advance(IrisPapiState.VIEW_TTL_MS);
assertEquals("reddunes", state.biomeCustomId(PLAYER));
assertEquals("reddunes", state.biomeCustomIds(PLAYER));
assertEquals(2, terrain.builds());
}
}
@@ -5,23 +5,43 @@ import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisRiverState;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class IrisTerrainSVCTest {
private static final IrisColumnQuery SMALL = IrisColumnQuery.rect(
@@ -53,6 +73,7 @@ public class IrisTerrainSVCTest {
assertEquals(IrisSurfaceKind.UNKNOWN, service.surfaceKind(null, 0, 0));
assertTrue(service.surfaceBiomeKey(null, 0, 0).isEmpty());
assertTrue(service.surfaceBiomeName(null, 0, 0).isEmpty());
assertTrue(service.surfaceBiomeInfo(null, 0, 0).isEmpty());
assertTrue(service.biomeKey(null, 0, 64, 0).isEmpty());
assertTrue(service.regionKey(null, 0, 0).isEmpty());
assertTrue(service.regionName(null, 0, 0).isEmpty());
@@ -66,6 +87,9 @@ public class IrisTerrainSVCTest {
assertEquals(Optional.class,
IrisTerrainService.class.getMethod("regionName", World.class, int.class, int.class)
.getReturnType());
assertEquals(Optional.class,
IrisTerrainService.class.getMethod("surfaceBiomeInfo", World.class, int.class, int.class)
.getReturnType());
}
@Test
@@ -94,6 +118,124 @@ public class IrisTerrainSVCTest {
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false)));
}
@Test
public void pendingBiomeMetadataReturnsAbsenceAndResolvesOnRetryWithoutLogging() throws Exception {
Engine engine = mock(Engine.class);
World world = world(engine);
IrisTerrainSVC service = enabledService();
IrisBiome biome = mock(IrisBiome.class);
when(biome.getLoadKey()).thenReturn("desert/dunes");
BiomeEnvironment environment = new BiomeEnvironment(1L, biome, mock(IrisRegion.class),
mock(IrisDimension.class), mock(IrisData.class));
when(engine.getSurfaceBiomeEnvironment(-17, 32))
.thenThrow(new SavedBiomeUnavailableException("Saved biome information is loading", true))
.thenReturn(environment);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
assertTrue(service.surfaceBiomeInfo(world, -17, 32).isEmpty());
assertEquals("desert/dunes", service.surfaceBiomeInfo(world, -17, 32).orElseThrow().key());
logging.verifyNoInteractions();
}
}
@Test
public void pendingReadsDoNotConsumeTheFirstRealFailureReport() throws Exception {
Engine engine = mock(Engine.class);
World world = world(engine);
IrisTerrainSVC service = enabledService();
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException(
"Saved biome data could not be read", new IllegalStateException("Invalid saved data"));
when(engine.getSurfaceBiomeEnvironment(-17, 32))
.thenThrow(new SavedBiomeUnavailableException("Saved biome information is loading", true))
.thenThrow(failure);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
assertTrue(service.surfaceBiomeInfo(world, -17, 32).isEmpty());
logging.verifyNoInteractions();
assertTrue(service.surfaceBiomeInfo(world, -17, 32).isEmpty());
logging.verify(() -> IrisLogging.reportError(contains("(1 terrain API query faults"), same(failure)));
logging.verifyNoMoreInteractions();
}
}
@Test
public void biomeAndRegionReadsTreatLoadingAsTemporaryAbsence() throws Exception {
Engine engine = mock(Engine.class);
World world = world(engine);
IrisTerrainSVC service = enabledService();
SavedBiomeUnavailableException pending = new SavedBiomeUnavailableException("Loading saved biomes", true);
when(engine.getSurfaceBiome(0, 0)).thenThrow(pending);
when(engine.getBiome(0, 64, 0)).thenThrow(pending);
when(engine.getRegion(0, 0)).thenThrow(pending);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
assertTrue(service.surfaceBiomeKey(world, 0, 0).isEmpty());
assertTrue(service.surfaceBiomeName(world, 0, 0).isEmpty());
assertTrue(service.biomeKey(world, 0, 64, 0).isEmpty());
assertTrue(service.regionKey(world, 0, 0).isEmpty());
assertTrue(service.regionName(world, 0, 0).isEmpty());
logging.verifyNoInteractions();
}
}
@Test
public void aLoadingReadWithASuppressedFailureIsStillReported() throws Exception {
Engine engine = mock(Engine.class);
World world = world(engine);
IrisTerrainSVC service = enabledService();
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Loading saved biomes", true);
failure.addSuppressed(new IllegalStateException("Failed to release saved definitions"));
when(engine.getSurfaceBiomeEnvironment(0, 0)).thenThrow(failure);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
assertTrue(service.surfaceBiomeInfo(world, 0, 0).isEmpty());
logging.verify(() -> IrisLogging.reportError(contains("surfaceBiomeInfo"), same(failure)));
logging.verifyNoMoreInteractions();
}
}
@Test
public void pendingBatchReadDoesNotHideSubsequentSinkFailures() throws Exception {
Engine engine = mock(Engine.class);
World world = world(engine);
IrisTerrainSVC service = enabledService();
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 0, 0, 1, EnumSet.of(IrisColumnField.BIOME_KEY));
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
when(engine.getSurfaceBiome(0, 0))
.thenThrow(new SavedBiomeUnavailableException("Loading saved biomes", true))
.thenReturn(mock(IrisBiome.class));
AtomicInteger sinkCalls = new AtomicInteger();
IllegalStateException failure = new IllegalStateException("Consumer failed");
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
assertFalse(service.sampleColumns(world, query, sample -> sinkCalls.incrementAndGet()));
assertEquals(0, sinkCalls.get());
logging.verifyNoInteractions();
assertFalse(service.sampleColumns(world, query, sample -> { throw failure; }));
logging.verify(() -> IrisLogging.reportError(contains("(1 terrain API sample faults"), same(failure)));
logging.verifyNoMoreInteractions();
}
}
private static World world(Engine engine) {
World world = mock(World.class);
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
when(world.getGenerator()).thenReturn(generator);
when(world.getName()).thenReturn("sandbox");
when(((PlatformChunkGenerator) generator).getEngine()).thenReturn(engine);
return world;
}
private static IrisTerrainSVC enabledService() throws ReflectiveOperationException {
IrisTerrainSVC service = new IrisTerrainSVC();
Field enabled = IrisTerrainSVC.class.getDeclaredField("serviceEnabled");
enabled.setAccessible(true);
((AtomicBoolean) enabled.get(service)).set(true);
return service;
}
private static HydrologyColumnSample river(boolean connectedFluid) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
1L,
@@ -0,0 +1,143 @@
package art.arcane.iris.platform.bukkit.terrain;
import art.arcane.iris.api.terrain.IrisBiomeInfo;
import art.arcane.iris.api.terrain.IrisCustomBiomeInfo;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class IrisBiomeInfoFactoryTest {
@Test
public void historicalMetadataUsesTheRetainedEnvironmentInsteadOfTheActivePack() {
IrisBiomeCustom custom = new IrisBiomeCustom().setId("amber-forest");
Fixture fixture = fixture(List.of(custom));
IrisData activeData = mock(IrisData.class);
when(fixture.engine().getData()).thenReturn(activeData);
when(fixture.data().customBiomeResourceKey(fixture.dimension(), custom)).thenReturn("iris:biomes/retained");
List<Throwable> failures = new ArrayList<>();
IrisBiomeInfo info = IrisBiomeInfoFactory.surface(fixture.engine(), -17, 32, failures::add);
assertEquals("forests/amber", info.key());
assertEquals("Amber Forest", info.name());
assertEquals("temperate", info.regionKey());
assertEquals("Temperate Region", info.regionName());
assertEquals("minecraft:forest", info.derivativeKey());
assertEquals("minecraft:birch_forest", info.vanillaDerivativeKey());
assertEquals("land", info.type());
assertEquals(List.of(new IrisCustomBiomeInfo("amber-forest", "iris:biomes/retained")), info.customDerivatives());
assertTrue(failures.isEmpty());
verify(fixture.engine()).getSurfaceBiomeEnvironment(-17, 32);
verifyNoMoreInteractions(fixture.engine());
verify(fixture.data()).customBiomeResourceKey(fixture.dimension(), custom);
verifyNoInteractions(activeData);
}
@Test
public void allCustomDerivativesRemainInAuthoredOrderWithoutSelectingAVariant() {
IrisBiomeCustom first = new IrisBiomeCustom().setId("amber");
IrisBiomeCustom second = new IrisBiomeCustom().setId("copper");
Fixture fixture = fixture(List.of(first, second));
when(fixture.data().customBiomeResourceKey(fixture.dimension(), first)).thenReturn("iris:biomes/first");
when(fixture.data().customBiomeResourceKey(fixture.dimension(), second)).thenReturn("iris:biomes/second");
IrisBiomeInfo info = IrisBiomeInfoFactory.surface(fixture.engine(), -17, 32, failure -> {
throw new AssertionError(failure);
});
assertEquals(List.of(
new IrisCustomBiomeInfo("amber", "iris:biomes/first"),
new IrisCustomBiomeInfo("copper", "iris:biomes/second")), info.customDerivatives());
verify(fixture.engine()).getSurfaceBiomeEnvironment(-17, 32);
verifyNoMoreInteractions(fixture.engine());
}
@Test
public void aVanillaBiomeDoesNotResolveAnyCustomRegistryKeys() {
Fixture fixture = fixture(List.of());
IrisBiomeInfo info = IrisBiomeInfoFactory.surface(fixture.engine(), -17, 32, failure -> {
throw new AssertionError(failure);
});
assertEquals("Amber Forest", info.name());
assertTrue(info.customDerivatives().isEmpty());
verifyNoInteractions(fixture.data());
}
@Test
public void aMissingMappingPreservesTheAuthoredIdAndOtherMetadata() {
IrisBiomeCustom first = new IrisBiomeCustom().setId("amber");
IrisBiomeCustom second = new IrisBiomeCustom().setId("copper");
Fixture fixture = fixture(List.of(first, second));
IllegalStateException missing = new IllegalStateException("Missing historical registry mapping");
when(fixture.data().customBiomeResourceKey(fixture.dimension(), first)).thenThrow(missing);
when(fixture.data().customBiomeResourceKey(fixture.dimension(), second)).thenReturn("iris:biomes/second");
List<Throwable> failures = new ArrayList<>();
IrisBiomeInfo info = IrisBiomeInfoFactory.surface(fixture.engine(), -17, 32, failures::add);
assertEquals("Amber Forest", info.name());
assertEquals(List.of(
new IrisCustomBiomeInfo("amber", ""),
new IrisCustomBiomeInfo("copper", "iris:biomes/second")), info.customDerivatives());
assertEquals(1, failures.size());
assertSame(missing, failures.getFirst().getCause());
assertTrue(failures.getFirst().getMessage().contains("amber"));
}
@Test
public void anUnclassifiedBiomeReportsAnEmptyType() {
Fixture fixture = fixture(List.of());
when(fixture.biome().getInferredType()).thenReturn(null);
IrisBiomeInfo info = IrisBiomeInfoFactory.surface(fixture.engine(), -17, 32, failure -> {
throw new AssertionError(failure);
});
assertEquals("", info.type());
}
private static Fixture fixture(List<IrisBiomeCustom> customBiomes) {
Engine engine = mock(Engine.class);
IrisBiome biome = mock(IrisBiome.class);
IrisRegion region = mock(IrisRegion.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisData data = mock(IrisData.class);
KList<IrisBiomeCustom> derivatives = new KList<>();
derivatives.addAll(customBiomes);
when(biome.getLoadKey()).thenReturn("forests/amber");
when(biome.getName()).thenReturn("Amber Forest");
when(biome.getDerivativeKey()).thenReturn("minecraft:forest");
when(biome.getVanillaDerivativeKey()).thenReturn("minecraft:birch_forest");
when(biome.getInferredType()).thenReturn(InferredType.LAND);
when(biome.getCustomDerivitives()).thenReturn(derivatives);
when(region.getLoadKey()).thenReturn("temperate");
when(region.getName()).thenReturn("Temperate Region");
when(engine.getSurfaceBiomeEnvironment(-17, 32)).thenReturn(
new BiomeEnvironment(4L, biome, region, dimension, data));
return new Fixture(engine, biome, dimension, data);
}
private record Fixture(Engine engine, IrisBiome biome, IrisDimension dimension, IrisData data) {
}
}