Fixed saved biome scoreboards

- Keep Studio board refreshes alive while saved biome data loads
- Sample mantle-aware biome environments and clear prior world values
- Report permanent failures once per display context
- Localize loading states and verify recovery and Jigsaw transitions
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-06 12:11:41 -04:00
1 parent 5570923d30
commit 3425c05e8b
23 files changed
+383 -3

No files matched your search

@@ -35,6 +35,14 @@ public final class BukkitUiMessages {
"iris.bukkit.scoreboard.biome",
C.AQUA + "Biome" + C.GRAY + ": {biome}"
);
public static final TextKey SCOREBOARD_BIOME_LOADING = TextKey.of(
"iris.bukkit.scoreboard.biome_loading",
"Loading"
);
public static final TextKey SCOREBOARD_BIOME_UNAVAILABLE = TextKey.of(
"iris.bukkit.scoreboard.biome_unavailable",
"Unavailable"
);
public static final TextKey SCOREBOARD_HEIGHT = TextKey.of(
"iris.bukkit.scoreboard.height",
C.AQUA + "Height" + C.GRAY + ": {height}"
@@ -56,6 +64,8 @@ public final class BukkitUiMessages {
SCOREBOARD_CARVING,
SCOREBOARD_REGION,
SCOREBOARD_BIOME,
SCOREBOARD_BIOME_LOADING,
SCOREBOARD_BIOME_UNAVAILABLE,
SCOREBOARD_HEIGHT,
SCOREBOARD_SLOPE,
SCOREBOARD_BLOCK_UPDATES
@@ -25,7 +25,9 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.board.Board;
import art.arcane.volmlib.util.board.BoardProvider;
@@ -358,6 +360,8 @@ public class BoardSVC implements IrisService, BoardProvider {
private volatile BoardView view;
private volatile boolean cancelled;
private volatile boolean ordinaryTickScheduled;
private UUID ordinaryWorldId;
private BiomeFailureContext reportedBiomeFailure;
public PlayerBoard(Player player) {
this.player = player;
@@ -377,10 +381,12 @@ public class BoardSVC implements IrisService, BoardProvider {
yieldBoard(player, this);
return;
}
boolean switched = view != BoardView.ORDINARY;
UUID worldId = player.getWorld().getUID();
boolean switched = view != BoardView.ORDINARY || !worldId.equals(ordinaryWorldId);
view = BoardView.ORDINARY;
jigsawContext = null;
if (switched) {
ordinaryWorldId = worldId;
updateOrdinary();
board.update();
}
@@ -515,14 +521,42 @@ public class BoardSVC implements IrisService, BoardProvider {
}
lines.add("&7&m ");
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, y, z).getName())));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName())));
String regionName;
String biomeName;
try {
BiomeEnvironment environment = engine.getBiomeOrMantleEnvironment(x, y, z);
regionName = environment.region().getName();
biomeName = environment.biome().getName();
reportedBiomeFailure = null;
} catch (SavedBiomeUnavailableException failure) {
regionName = IrisLanguage.text(failure.isLoading()
? BukkitUiMessages.SCOREBOARD_BIOME_LOADING
: BukkitUiMessages.SCOREBOARD_BIOME_UNAVAILABLE);
biomeName = regionName;
if (!failure.isLoading()) {
reportBiomeFailure(failure, engine, loc);
}
}
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", regionName)));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", biomeName)));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z)))));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getSlope(x, z), 2))));
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BLOCK_UPDATES, MessageArgument.trusted("updates", Form.f(engine.getBlockUpdatesPerSecond()))));
lines.add("&7&m ");
this.lines = lines;
}
private void reportBiomeFailure(SavedBiomeUnavailableException failure, Engine engine, Location location) {
World world = Objects.requireNonNull(location.getWorld());
BiomeFailureContext context = new BiomeFailureContext(world.getUID(), engine,
location.getBlockX() >> 4, location.getBlockZ() >> 4, failure.getMessage());
if (context.equals(reportedBiomeFailure)) {
return;
}
reportedBiomeFailure = context;
IrisLogging.reportError("Unable to display saved Iris biome information in " + world.getName()
+ " at " + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ() + ".", failure);
}
}
private void yieldBoard(Player player, PlayerBoard playerBoard) {
@@ -536,4 +570,7 @@ public class BoardSVC implements IrisService, BoardProvider {
ORDINARY,
JIGSAW
}
private record BiomeFailureContext(UUID worldId, Engine engine, int chunkX, int chunkZ, String reason) {
}
}
@@ -559,6 +559,18 @@ public class IrisEngine implements Engine {
}
}
@Override
public BiomeEnvironment getBiomeOrMantleEnvironment(int x, int y, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a mantle biome environment")) {
return Engine.super.getBiomeOrMantleEnvironment(x, y, z);
}
}
@Override
public BiomeEnvironment getSurfaceBiomeEnvironment(int x, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, 0, z, true);
@@ -97,6 +97,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return new BiomeEnvironment(0L, getBiome(x, y, z), getRegion(x, y, z), getDimension(), getData());
}
default BiomeEnvironment getBiomeOrMantleEnvironment(int x, int y, int z) {
return new BiomeEnvironment(0L, getBiomeOrMantle(x, y, z), getRegion(x, y, z), getDimension(), getData());
}
default BiomeEnvironment getSurfaceBiomeEnvironment(int x, int z) {
return new BiomeEnvironment(0L, getSurfaceBiome(x, z), getRegion(x, z), getDimension(), getData());
}
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dCarving§7: {state}",
"iris.bukkit.scoreboard.region": "§bRegion§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBiom§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Wird geladen",
"iris.bukkit.scoreboard.biome_unavailable": "Nicht verfügbar",
"iris.bukkit.scoreboard.height": "§bHöhe§7: {height}",
"iris.bukkit.scoreboard.slope": "§bSteigung§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dTallado§7: {state}",
"iris.bukkit.scoreboard.region": "§bRegión§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Cargando",
"iris.bukkit.scoreboard.biome_unavailable": "No disponible",
"iris.bukkit.scoreboard.height": "§bAltura§7: {height}",
"iris.bukkit.scoreboard.slope": "§bPendiente§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dKaiverrus§7: {state}",
"iris.bukkit.scoreboard.region": "§bAlue§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBiometri§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Ladataan",
"iris.bukkit.scoreboard.biome_unavailable": "Ei saatavilla",
"iris.bukkit.scoreboard.height": "§bKorkeus§7: {height}",
"iris.bukkit.scoreboard.slope": "§bRivi§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUDName/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dSculpture§7: {state}",
"iris.bukkit.scoreboard.region": "§bRégion§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBiome§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Chargement",
"iris.bukkit.scoreboard.biome_unavailable": "Indisponible",
"iris.bukkit.scoreboard.height": "§bHauteur§7: {height}",
"iris.bukkit.scoreboard.slope": "§bPente§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dעבודה§7: {state}",
"iris.bukkit.scoreboard.region": "§bאזור§7: {region}",
"iris.bukkit.scoreboard.biome": "§bביומי§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "בטעינה",
"iris.bukkit.scoreboard.biome_unavailable": "לא זמין",
"iris.bukkit.scoreboard.height": "§bגובה§7: {height}",
"iris.bukkit.scoreboard.slope": "§bסלייפ§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dCarving§7: {state}",
"iris.bukkit.scoreboard.region": "§bRegione§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Caricamento",
"iris.bukkit.scoreboard.biome_unavailable": "Non disponibile",
"iris.bukkit.scoreboard.height": "§bAltezza§7: {height}",
"iris.bukkit.scoreboard.slope": "§bPendenza§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dカービング§7: {state}",
"iris.bukkit.scoreboard.region": "§bリージョン§7: {region}",
"iris.bukkit.scoreboard.biome": "§bバイオーム§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "読み込み中",
"iris.bukkit.scoreboard.biome_unavailable": "利用不可",
"iris.bukkit.scoreboard.height": "§b高さ§7: {height}",
"iris.bukkit.scoreboard.slope": "§bスロープ§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§d조각 중§7: {state}",
"iris.bukkit.scoreboard.region": "§b주요 특징§7: {region}",
"iris.bukkit.scoreboard.biome": "§b바이오메트§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "불러오는 중",
"iris.bukkit.scoreboard.biome_unavailable": "사용 불가",
"iris.bukkit.scoreboard.height": "§b고도:§7: {height}",
"iris.bukkit.scoreboard.slope": "§b기울기§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dRaižyti§7: {state}",
"iris.bukkit.scoreboard.region": "§bRegionas§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBiomas§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Įkeliama",
"iris.bukkit.scoreboard.biome_unavailable": "Nepasiekiama",
"iris.bukkit.scoreboard.height": "§bAukštis§7: {height}",
"iris.bukkit.scoreboard.slope": "§bSpop§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBID/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dsnijden§7: {state}",
"iris.bukkit.scoreboard.region": "§bGebieden§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBiomeName§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Laden",
"iris.bukkit.scoreboard.biome_unavailable": "Niet beschikbaar",
"iris.bukkit.scoreboard.height": "§bHoogte§7: {height}",
"iris.bukkit.scoreboard.slope": "§bHelling§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD./s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dRzeźba§7: {state}",
"iris.bukkit.scoreboard.region": "§bObszar§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Ładowanie",
"iris.bukkit.scoreboard.biome_unavailable": "Niedostępne",
"iris.bukkit.scoreboard.height": "§bWysokość§7: {height}",
"iris.bukkit.scoreboard.slope": "§bNachylenie§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bPOD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dEsculpir§7: {state}",
"iris.bukkit.scoreboard.region": "§bRegião§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBioma§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "A carregar",
"iris.bukkit.scoreboard.biome_unavailable": "Indisponível",
"iris.bukkit.scoreboard.height": "§bAltura§7: {height}",
"iris.bukkit.scoreboard.slope": "§bDeslize§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUD./s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dрезьба§7: {state}",
"iris.bukkit.scoreboard.region": "§bрегион§7: {region}",
"iris.bukkit.scoreboard.biome": "§bбиом§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Загрузка",
"iris.bukkit.scoreboard.biome_unavailable": "Недоступно",
"iris.bukkit.scoreboard.height": "§bвысота§7: {height}",
"iris.bukkit.scoreboard.slope": "§bСклон§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bБУД/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dAraba§7: {state}",
"iris.bukkit.scoreboard.region": "§bBölge Bölgesi§7: {region}",
"iris.bukkit.scoreboard.biome": "§bBiyome§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Yükleniyor",
"iris.bukkit.scoreboard.biome_unavailable": "Kullanılamıyor",
"iris.bukkit.scoreboard.height": "§bYükseklik§7: {height}",
"iris.bukkit.scoreboard.slope": "§bSlopee§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBUDÜ/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§dQuan tâm§7: {state}",
"iris.bukkit.scoreboard.region": "§bVùng§7: {region}",
"iris.bukkit.scoreboard.biome": "§bĐôi§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "Đang tải",
"iris.bukkit.scoreboard.biome_unavailable": "Không khả dụng",
"iris.bukkit.scoreboard.height": "§bChiều cao§7: {height}",
"iris.bukkit.scoreboard.slope": "§bTrượt§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§bBAD/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§d雕刻§7: {state}",
"iris.bukkit.scoreboard.region": "§b地区§7: {region}",
"iris.bukkit.scoreboard.biome": "§b生物团§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "加载中",
"iris.bukkit.scoreboard.biome_unavailable": "不可用",
"iris.bukkit.scoreboard.height": "§b高度§7: {height}",
"iris.bukkit.scoreboard.slope": "§b斜坡§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§b学生会/s§7: {updates}",
@@ -1358,6 +1358,8 @@
"iris.bukkit.scoreboard.carving": "§d雕刻§7: {state}",
"iris.bukkit.scoreboard.region": "§b地區§7: {region}",
"iris.bukkit.scoreboard.biome": "§b生物團§7: {biome}",
"iris.bukkit.scoreboard.biome_loading": "載入中",
"iris.bukkit.scoreboard.biome_unavailable": "無法使用",
"iris.bukkit.scoreboard.height": "§b高度§7: {height}",
"iris.bukkit.scoreboard.slope": "§b斜坡§7: {slope}",
"iris.bukkit.scoreboard.block_updates": "§b學生會/s§7: {updates}",
@@ -0,0 +1,282 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.board.Board;
import art.arcane.volmlib.util.board.BoardSettings;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BoardSVCSavedBiomeTest {
private static IrisPlatform previousPlatform;
@BeforeClass
public static void bindPlatform() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class));
IrisPlatforms.bind(platform);
}
@AfterClass
public static void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void worldEntryLoadingKeepsRefreshAliveAndRecoversWithoutMovement() throws Exception {
try (Fixture fixture = new Fixture()) {
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved biome is loading", true);
fixture.service.on(new PlayerChangedWorldEvent(fixture.player, mock(World.class)));
assertTrue(fixture.lines().contains("Loading"));
assertEquals(1, fixture.ticks.size());
fixture.logging.verifyNoInteractions();
fixture.biomeFailure = null;
fixture.environment = environment("Saved region", "Saved biome");
fixture.tick();
assertTrue(fixture.lines().contains("Saved region"));
assertTrue(fixture.lines().contains("Saved biome"));
assertFalse(fixture.lines().contains("Loading"));
assertEquals(1, fixture.ticks.size());
verify(fixture.engine, times(2)).getBiomeOrMantleEnvironment(8, 160, 8);
verify(fixture.engine, never()).getRegion(anyInt(), anyInt(), anyInt());
verify(fixture.engine, never()).getBiomeOrMantle(anyInt(), anyInt(), anyInt());
}
}
@Test
public void unsavedBiomeUsesTheMantleOverride() throws Exception {
try (Fixture fixture = new Fixture()) {
IrisBiome natural = mock(IrisBiome.class);
IrisBiome flooded = mock(IrisBiome.class);
when(natural.getName()).thenReturn("Natural cave");
when(flooded.getName()).thenReturn("Flooded cave");
when(fixture.engine.getHeight(8, 8)).thenReturn(200);
when(fixture.engine.getCaveBiome(8, 160, 8)).thenReturn(natural);
when(fixture.engine.getCaveOrMantleBiome(8, 160, 8)).thenReturn(flooded);
when(fixture.engine.getDimension()).thenReturn(fixture.environment.dimension());
when(fixture.engine.getData()).thenReturn(fixture.environment.data());
doCallRealMethod().when(fixture.engine).getBiome(8, 160, 8);
doCallRealMethod().when(fixture.engine).getBiomeOrMantle(8, 160, 8);
doCallRealMethod().when(fixture.engine).getBiomeEnvironment(8, 160, 8);
doCallRealMethod().when(fixture.engine).getBiomeOrMantleEnvironment(8, 160, 8);
fixture.service.updatePlayer(fixture.player);
assertTrue(fixture.lines().contains("Flooded cave"));
assertFalse(fixture.lines().contains("Natural cave"));
verify(fixture.engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
}
}
@Test
public void unavailableBiomeReportsOncePerContextAndKeepsRefreshing() throws Exception {
try (Fixture fixture = new Fixture()) {
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved pack is missing", false);
fixture.service.updatePlayer(fixture.player);
assertTrue(fixture.lines().contains("Unavailable"));
assertFalse(fixture.lines().contains("Loading"));
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved pack is missing", false);
fixture.tick();
fixture.logging.verify(() -> IrisLogging.reportError(anyString(),
any(SavedBiomeUnavailableException.class)), times(1));
fixture.location = new Location(fixture.world, 24, 96, 8);
fixture.tick();
fixture.logging.verify(() -> IrisLogging.reportError(anyString(),
any(SavedBiomeUnavailableException.class)), times(2));
assertEquals(1, fixture.ticks.size());
}
}
@Test
public void changingWorldClearsThePreviousBiomeWhileTheNewOneLoads() throws Exception {
try (Fixture fixture = new Fixture()) {
fixture.environment = environment("Previous region", "Previous biome");
fixture.service.updatePlayer(fixture.player);
assertTrue(fixture.lines().contains("Previous biome"));
World previous = fixture.world;
fixture.world = world("second-studio");
fixture.location = new Location(fixture.world, 8, 96, 8);
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved biome is loading", true);
fixture.service.on(new PlayerChangedWorldEvent(fixture.player, previous));
assertTrue(fixture.lines().contains("Loading"));
assertFalse(fixture.lines().contains("Previous region"));
assertFalse(fixture.lines().contains("Previous biome"));
assertEquals(1, fixture.ticks.size());
}
}
@Test
public void jigsawViewDoesNotQueryBiomesAndOrdinaryViewStillRetries() throws Exception {
try (Fixture fixture = new Fixture()) {
fixture.biomeFailure = new SavedBiomeUnavailableException("Saved biome is loading", true);
JigsawStudioBoardContext context = new JigsawStudioBoardContext(
fixture.world.getUID(), UUID.randomUUID(), "village", JigsawStudioMode.PLANAR_JIGSAW,
"Corner", "Corner", "mossy", JigsawStudioBoardState.SAVED, "Open the chest");
fixture.service.applyJigsawContext(fixture.player, context);
assertEquals(BoardSVC.jigsawLines(context), fixture.service.getLines(fixture.player));
verify(fixture.engine, never()).getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt());
assertTrue(fixture.ticks.isEmpty());
fixture.service.clearJigsawContext(fixture.player);
assertTrue(fixture.lines().contains("Loading"));
assertFalse(fixture.lines().contains("Jigsaw Studio"));
assertEquals(1, fixture.ticks.size());
}
}
private static BiomeEnvironment environment(String regionName, String biomeName) {
IrisRegion region = mock(IrisRegion.class);
IrisBiome biome = mock(IrisBiome.class);
when(region.getName()).thenReturn(regionName);
when(biome.getName()).thenReturn(biomeName);
return new BiomeEnvironment(1L, biome, region, mock(IrisDimension.class), mock(IrisData.class));
}
private static World world(String name) {
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.randomUUID());
when(world.getName()).thenReturn(name);
when(world.getMinHeight()).thenReturn(-64);
return world;
}
private static void setField(BoardSVC service, String name, Object value) throws ReflectiveOperationException {
Field field = BoardSVC.class.getDeclaredField(name);
field.setAccessible(true);
field.set(service, value);
}
private static final class Fixture implements AutoCloseable {
private final MockedStatic<J> scheduling = mockStatic(J.class);
private final MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
private final MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class);
private final MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
private final MockedConstruction<Board> boards = mockConstruction(Board.class,
(board, context) -> when(board.ownsScoreboardAssignment()).thenReturn(true));
private final BoardSVC service = new BoardSVC();
private final Player player = mock(Player.class);
private final Engine engine = mock(Engine.class);
private final ArrayDeque<Runnable> ticks = new ArrayDeque<>();
private World world = world("first-studio");
private Location location = new Location(world, 8, 96, 8);
private BiomeEnvironment environment = environment("Ready region", "Ready biome");
private SavedBiomeUnavailableException biomeFailure;
private Fixture() throws ReflectiveOperationException {
setField(service, "boardEnabled", true);
setField(service, "settings", BoardSettings.builder().boardProvider(service).build());
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
when(player.isOnline()).thenReturn(true);
when(player.getWorld()).thenAnswer(invocation -> world);
when(player.getLocation()).thenAnswer(invocation -> location);
scheduling.when(() -> J.isOwnedByCurrentRegion(player)).thenReturn(true);
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(1, Runnable.class).run();
return true;
});
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(20), any(Runnable.class)))
.thenAnswer(invocation -> ticks.add(invocation.getArgument(1, Runnable.class)));
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
when(generator.isStudio()).thenReturn(true);
when(generator.getEngine()).thenReturn(engine);
toolbelt.when(() -> IrisToolbelt.access(any(World.class))).thenReturn(generator);
when(engine.getGeneratedPerSecond()).thenReturn(10D);
EngineMantle mantle = mock(EngineMantle.class);
when(engine.getMantle()).thenReturn(mantle);
doAnswer(invocation -> sampledEnvironment()).when(engine).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
doAnswer(invocation -> sampledEnvironment()).when(engine).getBiomeOrMantleEnvironment(anyInt(), anyInt(), anyInt());
doAnswer(invocation -> sampledEnvironment().region()).when(engine).getRegion(anyInt(), anyInt(), anyInt());
doAnswer(invocation -> sampledEnvironment().biome()).when(engine).getBiomeOrMantle(anyInt(), anyInt(), anyInt());
}
private BiomeEnvironment sampledEnvironment() {
if (biomeFailure != null) {
throw biomeFailure;
}
return environment;
}
private String lines() {
return String.join("\n", service.getLines(player));
}
private void tick() {
assertFalse("The ordinary scoreboard must schedule its next refresh", ticks.isEmpty());
ticks.remove().run();
}
@Override
public void close() {
boards.close();
logging.close();
settings.close();
toolbelt.close();
scheduling.close();
}
}
}
@@ -645,6 +645,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
when(saved.resolveCaveBase(19, -3)).thenReturn(Optional.of(environment));
assertSame(environment, engine.getBiomeEnvironment(19, 20, -3));
assertSame(environment, engine.getBiomeOrMantleEnvironment(19, 20, -3));
assertSame(environment, engine.getSurfaceBiomeEnvironment(19, -3));
assertSame(environment.biome(), engine.getBiome(19, 20, -3));
assertSame(environment.biome(), engine.getBiomeOrMantle(19, 20, -3));