From 3425c05e8b955c65d30e5b27ededcf396aa2071b Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 12:11:41 -0400 Subject: [PATCH] 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 --- .../core/localization/BukkitUiMessages.java | 10 + .../arcane/iris/core/service/BoardSVC.java | 43 ++- .../art/arcane/iris/engine/IrisEngine.java | 12 + .../arcane/iris/engine/framework/Engine.java | 4 + core/src/main/resources/languages/de_DE.json | 2 + core/src/main/resources/languages/es_ES.json | 2 + core/src/main/resources/languages/fi_FI.json | 2 + core/src/main/resources/languages/fr_FR.json | 2 + core/src/main/resources/languages/he_IL.json | 2 + core/src/main/resources/languages/it_IT.json | 2 + core/src/main/resources/languages/ja-JP.json | 2 + core/src/main/resources/languages/ko_KR.json | 2 + core/src/main/resources/languages/lt_LT.json | 2 + core/src/main/resources/languages/nl_NL.json | 2 + core/src/main/resources/languages/pl_PL.json | 2 + core/src/main/resources/languages/pt_PT.json | 2 + core/src/main/resources/languages/ru_RU.json | 2 + core/src/main/resources/languages/tr_TR.json | 2 + core/src/main/resources/languages/vi_VI.json | 2 + core/src/main/resources/languages/zh_CN.json | 2 + core/src/main/resources/languages/zh_TW.json | 2 + .../core/service/BoardSVCSavedBiomeTest.java | 282 ++++++++++++++++++ .../IrisEngineGenerationRuntimeScopeTest.java | 1 + 23 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/core/service/BoardSVCSavedBiomeTest.java diff --git a/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java b/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java index e9b070109..36df1b54a 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/BukkitUiMessages.java @@ -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 diff --git a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java index af26dbc55..0872d7b7b 100644 --- a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java @@ -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) { + } } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index ae0e99fa4..e7f32ec86 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -559,6 +559,18 @@ public class IrisEngine implements Engine { } } + @Override + public BiomeEnvironment getBiomeOrMantleEnvironment(int x, int y, int z) { + Optional 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 saved = resolveSavedBiomeEnvironment(x, 0, z, true); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index ceb6d4ca4..ad81331a2 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -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()); } diff --git a/core/src/main/resources/languages/de_DE.json b/core/src/main/resources/languages/de_DE.json index 3ca1aacdd..9b4354493 100644 --- a/core/src/main/resources/languages/de_DE.json +++ b/core/src/main/resources/languages/de_DE.json @@ -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}", diff --git a/core/src/main/resources/languages/es_ES.json b/core/src/main/resources/languages/es_ES.json index 3cb6ad977..e74a394c9 100644 --- a/core/src/main/resources/languages/es_ES.json +++ b/core/src/main/resources/languages/es_ES.json @@ -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}", diff --git a/core/src/main/resources/languages/fi_FI.json b/core/src/main/resources/languages/fi_FI.json index e251146d8..abcc6400d 100644 --- a/core/src/main/resources/languages/fi_FI.json +++ b/core/src/main/resources/languages/fi_FI.json @@ -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}", diff --git a/core/src/main/resources/languages/fr_FR.json b/core/src/main/resources/languages/fr_FR.json index 7351df21e..5054efcc7 100644 --- a/core/src/main/resources/languages/fr_FR.json +++ b/core/src/main/resources/languages/fr_FR.json @@ -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}", diff --git a/core/src/main/resources/languages/he_IL.json b/core/src/main/resources/languages/he_IL.json index 025a42d6e..e479a81cd 100644 --- a/core/src/main/resources/languages/he_IL.json +++ b/core/src/main/resources/languages/he_IL.json @@ -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}", diff --git a/core/src/main/resources/languages/it_IT.json b/core/src/main/resources/languages/it_IT.json index d89dd31ec..d086daed3 100644 --- a/core/src/main/resources/languages/it_IT.json +++ b/core/src/main/resources/languages/it_IT.json @@ -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}", diff --git a/core/src/main/resources/languages/ja-JP.json b/core/src/main/resources/languages/ja-JP.json index 7517c1e98..51a26bbe2 100644 --- a/core/src/main/resources/languages/ja-JP.json +++ b/core/src/main/resources/languages/ja-JP.json @@ -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}", diff --git a/core/src/main/resources/languages/ko_KR.json b/core/src/main/resources/languages/ko_KR.json index 9cdf2c228..7e5c3c931 100644 --- a/core/src/main/resources/languages/ko_KR.json +++ b/core/src/main/resources/languages/ko_KR.json @@ -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}", diff --git a/core/src/main/resources/languages/lt_LT.json b/core/src/main/resources/languages/lt_LT.json index b220c88a2..cb05cdb51 100644 --- a/core/src/main/resources/languages/lt_LT.json +++ b/core/src/main/resources/languages/lt_LT.json @@ -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}", diff --git a/core/src/main/resources/languages/nl_NL.json b/core/src/main/resources/languages/nl_NL.json index 3d9a4b356..a439f1557 100644 --- a/core/src/main/resources/languages/nl_NL.json +++ b/core/src/main/resources/languages/nl_NL.json @@ -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}", diff --git a/core/src/main/resources/languages/pl_PL.json b/core/src/main/resources/languages/pl_PL.json index d9a905191..7873ce982 100644 --- a/core/src/main/resources/languages/pl_PL.json +++ b/core/src/main/resources/languages/pl_PL.json @@ -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}", diff --git a/core/src/main/resources/languages/pt_PT.json b/core/src/main/resources/languages/pt_PT.json index af64a5de8..c5928cd4d 100644 --- a/core/src/main/resources/languages/pt_PT.json +++ b/core/src/main/resources/languages/pt_PT.json @@ -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}", diff --git a/core/src/main/resources/languages/ru_RU.json b/core/src/main/resources/languages/ru_RU.json index 958d8042f..5c59e40aa 100644 --- a/core/src/main/resources/languages/ru_RU.json +++ b/core/src/main/resources/languages/ru_RU.json @@ -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}", diff --git a/core/src/main/resources/languages/tr_TR.json b/core/src/main/resources/languages/tr_TR.json index e45953a8a..13489820d 100644 --- a/core/src/main/resources/languages/tr_TR.json +++ b/core/src/main/resources/languages/tr_TR.json @@ -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}", diff --git a/core/src/main/resources/languages/vi_VI.json b/core/src/main/resources/languages/vi_VI.json index d2b79adf3..e2ce21d00 100644 --- a/core/src/main/resources/languages/vi_VI.json +++ b/core/src/main/resources/languages/vi_VI.json @@ -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}", diff --git a/core/src/main/resources/languages/zh_CN.json b/core/src/main/resources/languages/zh_CN.json index 42e6230b7..8dc0d74f3 100644 --- a/core/src/main/resources/languages/zh_CN.json +++ b/core/src/main/resources/languages/zh_CN.json @@ -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}", diff --git a/core/src/main/resources/languages/zh_TW.json b/core/src/main/resources/languages/zh_TW.json index 9513a8cf3..e3999f1a9 100644 --- a/core/src/main/resources/languages/zh_TW.json +++ b/core/src/main/resources/languages/zh_TW.json @@ -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}", diff --git a/core/src/test/java/art/arcane/iris/core/service/BoardSVCSavedBiomeTest.java b/core/src/test/java/art/arcane/iris/core/service/BoardSVCSavedBiomeTest.java new file mode 100644 index 000000000..b5e295861 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/service/BoardSVCSavedBiomeTest.java @@ -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 scheduling = mockStatic(J.class); + private final MockedStatic toolbelt = mockStatic(IrisToolbelt.class); + private final MockedStatic settings = mockStatic(IrisSettings.class); + private final MockedStatic logging = mockStatic(IrisLogging.class); + private final MockedConstruction 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 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(); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java index c4f3c86da..8d26b2227 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java @@ -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));