diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java index 44e360ebe..cb92dd94d 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java @@ -1895,6 +1895,72 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0)); } + @Test + public void loweredSnowSurfaceRemovesSolidSlabDecoration() { + Map blocks = new HashMap<>(); + for (int y = 82; y < 99; y++) { + put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState()); + } + put(blocks, 0, 99, 0, Blocks.SNOW_BLOCK.defaultBlockState()); + put(blocks, 0, 100, 0, Blocks.COBBLESTONE_SLAB.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 99, 83, -64, 319); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 82, 0)); + assertEquals(Blocks.SNOW_BLOCK.defaultBlockState(), state(blocks, 0, 83, 0)); + for (int y = 84; y <= 100; y++) { + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0)); + } + } + + @Test + public void loweredSurfaceRemovesStackedSolidDecorationUntilAir() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + put(blocks, 0, 66, 0, Blocks.COBBLESTONE_STAIRS.defaultBlockState()); + put(blocks, 0, 67, 0, Blocks.COBBLESTONE_WALL.defaultBlockState()); + put(blocks, 0, 69, 0, Blocks.STONE.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 62, 0)); + for (int y = 63; y <= 68; y++) { + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0)); + } + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 69, 0)); + } + + @Test + public void loweredSurfacePreservesTreeDecorationBoundary() { + for (BlockState tree : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 65, 0, Blocks.COBBLESTONE_SLAB.defaultBlockState()); + put(blocks, 0, 66, 0, tree); + put(blocks, 0, 67, 0, Blocks.STONE.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 62, 0)); + for (int y = 63; y <= 65; y++) { + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0)); + } + assertEquals(tree, state(blocks, 0, 66, 0)); + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 67, 0)); + } + } + @Test public void rigidSurfaceBaseClosesAOneBlockSubsurfaceGap() { Map blocks = new HashMap<>(); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java index daa609bc6..b827a9593 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java @@ -239,8 +239,8 @@ public class NativeStructurePostProcessorVegetationTest { } @Test - public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() { - for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) { + public void loweringSurfaceKeepsTreeBoundaries() { + for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) { ProtoChunk chunk = chunk(); write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState()); write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState()); @@ -257,6 +257,34 @@ public class NativeStructurePostProcessorVegetationTest { } } + @Test + public void loweringSurfaceRemovesUnsupportedPlankDecoration() { + ProtoChunk chunk = chunk(); + for (int x : List.of(4, 5)) { + for (int y = 60; y <= 64; y++) { + write(chunk, x, y, 4, Blocks.SAND.defaultBlockState()); + } + write(chunk, x, 65, 4, Blocks.OAK_PLANKS.defaultBlockState()); + write(chunk, x, 66, 4, Blocks.CACTUS.defaultBlockState()); + write(chunk, x, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState()); + } + WorldGenLevel world = world(chunk); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y); + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y); + + assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 60, 4))); + for (int y = 61; y <= 67; y++) { + assertTrue(chunk.getBlockState(new BlockPos(4, y, 4)).isAir()); + } + assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4))); + assertEquals(Blocks.OAK_PLANKS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4))); + assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4))); + assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 67, 4))); + } + private static void clearVegetation(ProtoChunk chunk, StructureStart start) { NativeStructureVegetationClearer.clearIntersectingVegetation( world(chunk), chunk, AREA, List.of(start)); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java index 38660ffc1..f644ab532 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorSurfaceTerrainTest.java @@ -1895,6 +1895,72 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0)); } + @Test + public void loweredSnowSurfaceRemovesSolidSlabDecoration() { + Map blocks = new HashMap<>(); + for (int y = 82; y < 99; y++) { + put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState()); + } + put(blocks, 0, 99, 0, Blocks.SNOW_BLOCK.defaultBlockState()); + put(blocks, 0, 100, 0, Blocks.COBBLESTONE_SLAB.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 99, 83, -64, 319); + + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 82, 0)); + assertEquals(Blocks.SNOW_BLOCK.defaultBlockState(), state(blocks, 0, 83, 0)); + for (int y = 84; y <= 100; y++) { + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0)); + } + } + + @Test + public void loweredSurfaceRemovesStackedSolidDecorationUntilAir() { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 65, 0, Blocks.COBBLESTONE.defaultBlockState()); + put(blocks, 0, 66, 0, Blocks.COBBLESTONE_STAIRS.defaultBlockState()); + put(blocks, 0, 67, 0, Blocks.COBBLESTONE_WALL.defaultBlockState()); + put(blocks, 0, 69, 0, Blocks.STONE.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 62, 0)); + for (int y = 63; y <= 68; y++) { + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0)); + } + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 69, 0)); + } + + @Test + public void loweredSurfacePreservesTreeDecorationBoundary() { + for (BlockState tree : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) { + Map blocks = new HashMap<>(); + put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 65, 0, Blocks.COBBLESTONE_SLAB.defaultBlockState()); + put(blocks, 0, 66, 0, tree); + put(blocks, 0, 67, 0, Blocks.STONE.defaultBlockState()); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 62, -64, 319); + + assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 62, 0)); + for (int y = 63; y <= 65; y++) { + assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, y, 0)); + } + assertEquals(tree, state(blocks, 0, 66, 0)); + assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 67, 0)); + } + } + @Test public void rigidSurfaceBaseClosesAOneBlockSubsurfaceGap() { Map blocks = new HashMap<>(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java index f47dbe3fb..07581aa72 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeStructurePostProcessorVegetationTest.java @@ -239,8 +239,8 @@ public class NativeStructurePostProcessorVegetationTest { } @Test - public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() { - for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) { + public void loweringSurfaceKeepsTreeBoundaries() { + for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_LEAVES.defaultBlockState())) { ProtoChunk chunk = chunk(); write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState()); write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState()); @@ -257,6 +257,34 @@ public class NativeStructurePostProcessorVegetationTest { } } + @Test + public void loweringSurfaceRemovesUnsupportedPlankDecoration() { + ProtoChunk chunk = chunk(); + for (int x : List.of(4, 5)) { + for (int y = 60; y <= 64; y++) { + write(chunk, x, y, 4, Blocks.SAND.defaultBlockState()); + } + write(chunk, x, 65, 4, Blocks.OAK_PLANKS.defaultBlockState()); + write(chunk, x, 66, 4, Blocks.CACTUS.defaultBlockState()); + write(chunk, x, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState()); + } + WorldGenLevel world = world(chunk); + + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y); + NativeStructureSurfaceFitter.applySurfaceColumn( + world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y); + + assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 60, 4))); + for (int y = 61; y <= 67; y++) { + assertTrue(chunk.getBlockState(new BlockPos(4, y, 4)).isAir()); + } + assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4))); + assertEquals(Blocks.OAK_PLANKS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4))); + assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4))); + assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 67, 4))); + } + private static void clearVegetation(ProtoChunk chunk, StructureStart start) { NativeStructureVegetationClearer.clearIntersectingVegetation( world(chunk), chunk, AREA, List.of(start)); diff --git a/adapters/bukkit/plugin/src/test/gameplay/dust-revealer.mjs b/adapters/bukkit/plugin/src/test/gameplay/dust-revealer.mjs new file mode 100644 index 000000000..1417aa083 --- /dev/null +++ b/adapters/bukkit/plugin/src/test/gameplay/dust-revealer.mjs @@ -0,0 +1,49 @@ +export default { + name: 'iris-dust-revealer', + description: 'Inspect generated terrain with Dust of Revealing and retry pending saved-biome reads.', + async run(context) { + const bot = context.bot; + const returnPosition = bot.entity.position.clone(); + await context.step('create an Iris terrain world', async () => { + const response = await context.command('/irislifecycle terrain-create', /NATIVE_TERRAIN seed=|NATIVE_FAILED/, 180000); + context.expect(response.includes('NATIVE_TERRAIN seed=78264193 columns=16 biome=plains'), response); + }); + await context.step('enter the generated terrain and receive reveal dust', async () => { + await context.command(`/execute in iris:native_terrain_probe run tp ${bot.username} 34 101 -46`, /Teleported/, 10000); + await context.waitForEvent('physicsTick', () => bot.entity.onGround && Math.abs(bot.entity.position.x - 34) < 1, 15000); + const received = context.waitForEvent('physicsTick', () => bot.inventory.items().some(item => item.name === 'glowstone_dust'), 10000); + await context.command('/ir o d'); + await received; + const dust = bot.inventory.items().find(item => item.name === 'glowstone_dust'); + context.expect(dust !== undefined, 'Dust command did not grant glowstone dust'); + await bot.equip(dust, 'hand'); + }); + for (let inspection = 0; inspection < 2; inspection++) { + await context.step(`inspect the block with reveal dust ${inspection + 1}`, async () => { + const target = bot.findBlock({ matching: block => block.name === 'lime_wool', maxDistance: 4 }); + context.expect(target !== null, 'Generated terrain block is not within reach'); + for (let attempt = 0; attempt < 10; attempt++) { + const start = context.report.messages.length; + const response = context.waitForMessage(/Saved biome information is loading|Click to copy these stats|Object reveal failed/, 10000); + await bot.activateBlock(target); + const message = await response; + if (message.includes('Saved biome information is loading')) { + await context.sleep(100); + continue; + } + context.expect(message.includes('Click to copy these stats'), message); + const messages = context.report.messages.slice(start).map(entry => entry.message).join('\n'); + context.expect(/Surface biome: flat/.test(messages), messages); + context.expect(/Region: flat/.test(messages), messages); + return; + } + context.expect(false, 'Saved biome information remained loading after ten inspections'); + }); + } + await context.step('leave and unload the inspected world', async () => { + await context.command(`/execute in minecraft:overworld run tp ${bot.username} ${returnPosition.x} ${returnPosition.y} ${returnPosition.z}`, /Teleported/, 10000); + const response = await context.command('/irislifecycle terrain-unload', /NATIVE_TERRAIN_UNLOADED|NATIVE_FAILED/, 90000); + context.expect(response.includes('NATIVE_TERRAIN_UNLOADED true'), response); + }); + } +}; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java index 4fb49c990..9ef8703e4 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDustRevealer.java @@ -29,6 +29,7 @@ import art.arcane.iris.generation.runtime.GenerationSessionException; import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.iris.world.history.SavedBiomeUnavailableException; import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState; import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedScheduler; @@ -66,12 +67,22 @@ public final class ModdedDustRevealer { IrisLanguage.plain(RuntimeUiMessages.DUST_IRIS_WORLD_REQUIRED))); return; } - describe(player, level, engine, pos); - int relativeY = pos.y() - engine.getMinHeight(); - String key = safe( - "object lookup at " + coordinates(pos), - () -> engine.getObjectPlacementKey(pos.x(), relativeY, pos.z())); + String key; + try { + describe(player, level, engine, pos); + key = safe( + "object lookup at " + coordinates(pos), + () -> engine.getObjectPlacementKey(pos.x(), relativeY, pos.z())); + } catch (SavedBiomeUnavailableException unavailable) { + if (!unavailable.isLoading()) { + ModdedIrisLog.error("Iris dust saved biome lookup failed at {}", coordinates(pos), unavailable); + } + player.sendSystemMessage(NativeCommandText.literal(IrisLanguage.plain(unavailable.isLoading() + ? RuntimeUiMessages.DUST_BIOME_LOADING + : RuntimeUiMessages.DUST_REVEAL_FAILED))); + return; + } if (key == null) { return; } @@ -435,6 +446,8 @@ public final class ModdedDustRevealer { private static T safe(String operation, Supplier supplier) { try { return supplier.get(); + } catch (SavedBiomeUnavailableException unavailable) { + throw unavailable; } catch (Throwable error) { ModdedIrisLog.error("Iris dust {} failed", operation, error); return null; diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java index 2ca05ff4a..f096efb51 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedBlockResolutionContractTest.java @@ -84,6 +84,25 @@ public class ModdedBlockResolutionContractTest { } } + @Test + public void netherGroundCoverAcceptsNyliumAndSoulSoilButRejectsBarrenBlocks() { + for (Block plant : new Block[]{Blocks.CRIMSON_ROOTS, Blocks.WARPED_ROOTS, Blocks.NETHER_SPROUTS}) { + for (Block substrate : new Block[]{Blocks.CRIMSON_NYLIUM, Blocks.WARPED_NYLIUM, + Blocks.SOUL_SOIL, Blocks.GRASS_BLOCK, Blocks.COARSE_DIRT, Blocks.PODZOL}) { + assertTrue(plant + " on " + substrate, NativeBlockProperties.canPlaceOnto(plant, substrate)); + } + for (Block substrate : new Block[]{Blocks.NETHERRACK, Blocks.SOUL_SAND, + Blocks.STONE, Blocks.GRAVEL, Blocks.AIR, Blocks.WATER}) { + assertFalse(plant + " on " + substrate, NativeBlockProperties.canPlaceOnto(plant, substrate)); + } + } + for (Block fungus : new Block[]{Blocks.CRIMSON_FUNGUS, Blocks.WARPED_FUNGUS}) { + for (Block substrate : new Block[]{Blocks.CRIMSON_NYLIUM, Blocks.WARPED_NYLIUM, Blocks.SOUL_SOIL}) { + assertTrue(fungus + " on " + substrate, NativeBlockProperties.canPlaceOnto(fungus, substrate)); + } + } + } + @Test public void sulfurAndDripstoneTipsReceivePostLoadUpdates() { for (Block block : new Block[]{Blocks.SULFUR_SPIKE, Blocks.POINTED_DRIPSTONE}) { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java index c1e18ec8b..c8baf82ae 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedDustRevealerTest.java @@ -1,7 +1,23 @@ package art.arcane.iris.modded.command; +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.localization.IrisLanguage; +import art.arcane.iris.localization.RuntimeUiMessages; +import art.arcane.iris.modded.ModdedIrisLog; +import art.arcane.iris.world.history.SavedBiomeUnavailableException; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEditPlayer; +import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEditWorld; import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.nativelib.terrain.NativeWorld; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; import java.util.List; import java.util.Set; @@ -9,8 +25,90 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class ModdedDustRevealerTest { + @BeforeClass + public static void initializeRuntimeTypes() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + public void loadingBiomesShowOneNoticeAndNextInteractionShowsResolvedDetails() { + Engine engine = mock(Engine.class); + NativeEditPlayer player = mock(NativeEditPlayer.class); + NativeEditWorld level = mock(NativeEditWorld.class); + NativeWorld world = mock(NativeWorld.class); + NativeBlockState block = mock(NativeBlockState.class); + NativeBlockPoint position = new NativeBlockPoint(4, 80, 7); + IrisBiome biome = new IrisBiome(); + biome.setLoadKey("saved-forest"); + when(level.world()).thenReturn(world); + when(level.block(4, 80, 7)).thenReturn(block); + when(block.key()).thenReturn("minecraft:stone"); + when(level.biome(position)).thenReturn(new NativeEditWorld.BiomeIdentity("minecraft:forest", 4)); + when(engine.getSurfaceBiome(4, 7)) + .thenThrow(new SavedBiomeUnavailableException("Loading", true)) + .thenReturn(biome); + + try (MockedStatic commands = mockStatic(IrisModdedCommands.class); + MockedStatic logging = mockStatic(ModdedIrisLog.class)) { + commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine); + + ModdedDustRevealer.reveal(player, level, position); + + ArgumentCaptor notice = ArgumentCaptor.forClass(NativeCommandText.class); + verify(player).sendSystemMessage(notice.capture()); + assertEquals(IrisLanguage.plain(RuntimeUiMessages.DUST_BIOME_LOADING), notice.getValue().getString()); + verify(engine, never()).getBiome(4, 80, 7); + verify(level, never()).sound(any(), any(), any()); + logging.verifyNoInteractions(); + + clearInvocations(player); + ModdedDustRevealer.reveal(player, level, position); + + verify(player).sendSystemMessage(argThat( + message -> message.getString().startsWith("Surface biome: saved-forest"))); + verify(player, never()).sendSystemMessage(argThat( + message -> message.getString().equals(IrisLanguage.plain(RuntimeUiMessages.DUST_BIOME_LOADING)))); + logging.verifyNoInteractions(); + } + } + + @Test + public void unavailableSavedBiomesRemainLoggedAndAbortReveal() { + Engine engine = mock(Engine.class); + NativeEditPlayer player = mock(NativeEditPlayer.class); + NativeEditWorld level = mock(NativeEditWorld.class); + NativeWorld world = mock(NativeWorld.class); + SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Missing saved pack", false); + when(level.world()).thenReturn(world); + when(engine.getSurfaceBiome(4, 7)).thenThrow(failure); + + try (MockedStatic commands = mockStatic(IrisModdedCommands.class); + MockedStatic logging = mockStatic(ModdedIrisLog.class)) { + commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine); + + ModdedDustRevealer.reveal(player, level, new NativeBlockPoint(4, 80, 7)); + + ArgumentCaptor notice = ArgumentCaptor.forClass(NativeCommandText.class); + verify(player).sendSystemMessage(notice.capture()); + assertEquals(IrisLanguage.plain(RuntimeUiMessages.DUST_REVEAL_FAILED), notice.getValue().getString()); + logging.verify(() -> ModdedIrisLog.error("Iris dust saved biome lookup failed at {}", "4, 80, 7", failure)); + logging.verifyNoMoreInteractions(); + verify(engine, never()).getBiome(4, 80, 7); + verify(level, never()).sound(any(), any(), any()); + } + } + @Test public void placementClassificationCoversObjectDecorationAndTerrain() { assertTrue(ModdedDustRevealer.placementLine(3, 10, 13, "tree") diff --git a/build.gradle b/build.gradle index f08bb4878..376c00ed0 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ import art.arcane.volmit.packaging.NativeDevelopmentTargets import io.papermc.paperweight.userdev.PaperweightUser import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension import io.papermc.paperweight.userdev.PaperweightUserExtension +import org.gradle.api.artifacts.component.ComponentIdentifier import org.gradle.api.plugins.JavaPluginExtension import org.gradle.jvm.toolchain.JavaToolchainService import org.gradle.api.file.CopySpec @@ -178,30 +179,48 @@ nmsBindings.each { String key -> def included = configurations.create('included') def nativeRuntime = configurations.create('nativeRuntime') +def publishedNativeRuntime = configurations.create('publishedNativeRuntime') { + canBeConsumed = false + transitive = false + resolutionStrategy.useGlobalDependencySubstitutionRules = false +} +repositories.configureEach { repository -> + if (repository.name != 'publishedNativeProviders') { + repository.content { notForConfigurations('publishedNativeRuntime') } + } +} +repositories.maven { + name = 'publishedNativeProviders' + url = uri('https://jitpack.io/') + content { + onlyForConfigurations('publishedNativeRuntime') + includeGroup('com.github.VolmitSoftware.VolmLib') + } +} def nativeRuntimeOutput = layout.buildDirectory.dir('generated/native-runtime') def nativeRuntimeRules = layout.buildDirectory.file('generated/native-runtime-keep.pro') +List nativeRuntimeModules = ['native-common'] + nmsBindings.collect { String adapter -> 'native-' + adapter } dependencies { - (['native-common'] + nmsBindings.collect { String adapter -> 'native-' + adapter }).each { String module -> + nativeRuntimeModules.each { String module -> add('nativeRuntime', "com.github.VolmitSoftware.VolmLib:${module}:${volmLibCoordinate.tokenize(':').last()}") { transitive = false } + add('publishedNativeRuntime', "com.github.VolmitSoftware.VolmLib:${module}:${volmLibCoordinate.tokenize(':').last()}") } } def generateNativeRuntime = tasks.register('generateNativeRuntime') { - inputs.files(nativeRuntime) + inputs.files(publishedNativeRuntime) inputs.property('version', volmLibCoordinate.tokenize(':').last()) inputs.property('adapters', nmsBindings) outputs.dir(nativeRuntimeOutput) outputs.file(nativeRuntimeRules) doLast { - Map artifacts = new TreeMap<>() - nativeRuntime.resolvedConfiguration.resolvedArtifacts.each { artifact -> - String module = artifact.moduleVersion.id.name - if (module.startsWith('v') && module.contains('_R')) { - module = 'native-' + module - } - artifacts.put(module, artifact.file) + Map resolved = new LinkedHashMap<>() + publishedNativeRuntime.incoming.artifacts.artifacts.each { artifact -> + resolved.put(artifact.id.componentIdentifier, artifact.file) } + Map artifacts = NativeRuntimeArtifacts.publishedArtifacts(resolved, + nativeRuntimeModules, volmLibCoordinate.tokenize(':').last()) NativeRuntimeArtifacts.generate(artifacts, volmLibCoordinate.tokenize(':').last(), nativeRuntimeOutput.get().asFile, nativeRuntimeRules.get().asFile) } diff --git a/buildSrc/src/main/java/NativeRuntimeArtifacts.java b/buildSrc/src/main/java/NativeRuntimeArtifacts.java index 3cca17fd7..e49bba3ed 100644 --- a/buildSrc/src/main/java/NativeRuntimeArtifacts.java +++ b/buildSrc/src/main/java/NativeRuntimeArtifacts.java @@ -1,4 +1,6 @@ import org.gradle.api.GradleException; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import java.io.File; import java.io.IOException; @@ -15,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -27,6 +30,27 @@ public final class NativeRuntimeArtifacts { private NativeRuntimeArtifacts() { } + public static Map publishedArtifacts(Map resolved, + List modules, String version) { + Map artifacts = new TreeMap<>(); + for (Map.Entry entry : resolved.entrySet()) { + if (!(entry.getKey() instanceof ModuleComponentIdentifier component) + || !GROUP.equals(component.getGroup()) || !version.equals(component.getVersion()) + || !modules.contains(component.getModule())) { + throw new GradleException("Native runtime providers must resolve to published coordinates at " + + version + ": " + entry.getKey().getDisplayName()); + } + if (artifacts.put(component.getModule(), entry.getValue()) != null) { + throw new GradleException("Duplicate native runtime provider: " + component.getModule()); + } + } + if (!artifacts.keySet().equals(new TreeSet<>(modules))) { + throw new GradleException("Published native runtime providers differ: " + artifacts.keySet() + + "; expected " + modules); + } + return artifacts; + } + public static void generate(Map artifacts, String version, File output, File rules) throws IOException { List manifest = new ArrayList<>(); manifest.add("modules=" + String.join(",", artifacts.keySet())); diff --git a/buildSrc/src/test/java/NativeRuntimeArtifactsTest.java b/buildSrc/src/test/java/NativeRuntimeArtifactsTest.java index dcc752210..52d9df462 100644 --- a/buildSrc/src/test/java/NativeRuntimeArtifactsTest.java +++ b/buildSrc/src/test/java/NativeRuntimeArtifactsTest.java @@ -1,20 +1,66 @@ import org.gradle.api.GradleException; +import org.gradle.api.artifacts.ModuleIdentifier; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import org.junit.Test; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import java.io.File; +import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.HexFormat; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class NativeRuntimeArtifactsTest { + @Test + public void acceptsOnlyRequestedPublishedCoordinates() { + File common = new File("common.jar"); + File provider = new File("provider.jar"); + Map resolved = Map.of( + module("native-common", "revision"), common, + module("native-v26_2_R1", "revision"), provider); + assertEquals(Map.of("native-common", common, "native-v26_2_R1", provider), + NativeRuntimeArtifacts.publishedArtifacts(resolved, + List.of("native-common", "native-v26_2_R1"), "revision")); + } + + @Test + public void rejectsCompositeProjectArtifacts() { + ComponentIdentifier project = () -> "project :VolmLib:native-common"; + assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.publishedArtifacts( + Map.of(project, new File("native-common-local-SNAPSHOT.jar")), List.of("native-common"), "revision")); + } + + @Test + public void rejectsUnexpectedCoordinates() { + List invalid = List.of( + new PublishedModule("other.group", "native-common", "revision"), + module("native-common", "other-revision"), + module("native-v26_3_R1", "revision")); + for (ComponentIdentifier component : invalid) { + assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.publishedArtifacts( + Map.of(component, new File("provider.jar")), List.of("native-common"), "revision")); + } + } + + @Test + public void rejectsMissingProviders() { + assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.publishedArtifacts( + Map.of(module("native-common", "revision"), new File("common.jar")), + List.of("native-common", "native-v26_2_R1"), "revision")); + } + @Test public void recordsExactDependenciesAndRetainsSharedProviderReferences() throws Exception { Path directory = Files.createTempDirectory("native-artifacts"); @@ -31,6 +77,14 @@ public class NativeRuntimeArtifactsTest { File rules = directory.resolve("keep.pro").toFile(); NativeRuntimeArtifacts.generate(Map.of("native-common", provider, "native-v26_2_R1", provider), "revision", directory.toFile(), rules); + Properties manifest = new Properties(); + try (Reader reader = Files.newBufferedReader(directory.resolve(NativeRuntimeArtifacts.MANIFEST))) { + manifest.load(reader); + } + String digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(Files.readAllBytes(provider.toPath()))); + assertEquals(digest, manifest.getProperty("native-common.sha256")); + assertEquals(digest, manifest.getProperty("native-v26_2_R1.sha256")); assertTrue(Files.readString(rules.toPath()).contains("-keep class art.arcane.volmlib.util.collection.KList { *; }")); File plugin = directory.resolve("plugin.jar").toFile(); try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(plugin.toPath()))) { @@ -42,4 +96,41 @@ public class NativeRuntimeArtifactsTest { assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin, List.of("v26_3_R1"), "revision")); assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin, List.of("v26_2_R1"), "other")); } + + private static ModuleComponentIdentifier module(String module, String version) { + return new PublishedModule("com.github.VolmitSoftware.VolmLib", module, version); + } + + private record PublishedModule(String group, String module, String version) + implements ModuleComponentIdentifier, ModuleIdentifier { + @Override + public String getGroup() { + return group; + } + + @Override + public String getModule() { + return module; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public ModuleIdentifier getModuleIdentifier() { + return this; + } + + @Override + public String getName() { + return module; + } + + @Override + public String getDisplayName() { + return group + ":" + module + ":" + version; + } + } } diff --git a/core/minecraft-purity-allowlist.txt b/core/minecraft-purity-allowlist.txt index 3c4708e81..e69de29bb 100644 --- a/core/minecraft-purity-allowlist.txt +++ b/core/minecraft-purity-allowlist.txt @@ -1,6 +0,0 @@ -art/arcane/iris/diagnostics/LogFilterSVC.java -art/arcane/iris/structure/BulkStructureImporter.java -art/arcane/iris/structure/VillageImporter.java -art/arcane/iris/world/lifecycle/CapabilitySnapshot.java -art/arcane/iris/world/lifecycle/WorldLifecycleSupport.java -art/arcane/iris/world/runtime/WorldRuntimeControlService.java diff --git a/core/purity-allowlist.txt b/core/purity-allowlist.txt index 4837aca58..0d263819a 100644 --- a/core/purity-allowlist.txt +++ b/core/purity-allowlist.txt @@ -66,6 +66,7 @@ art/arcane/iris/pack/validation/IrisCompatabilityBlockFilter.java art/arcane/iris/pack/validation/IrisCompatabilityItemFilter.java art/arcane/iris/pack/value/IrisDirection.java art/arcane/iris/platform/bootstrap/Bindings.java +art/arcane/iris/platform/bootstrap/SlimJar.java art/arcane/iris/platform/bukkit/BoardSVC.java art/arcane/iris/platform/bukkit/BukkitBiome.java art/arcane/iris/platform/bukkit/BukkitBlockResolution.java @@ -80,6 +81,12 @@ art/arcane/iris/platform/bukkit/BukkitWorld.java art/arcane/iris/platform/bukkit/BukkitWorldBinding.java art/arcane/iris/platform/bukkit/chunk/ChunkTickets.java art/arcane/iris/platform/bukkit/chunk/TicketHolder.java +art/arcane/iris/platform/bukkit/nms/BukkitBinding.java +art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java +art/arcane/iris/platform/bukkit/nms/BukkitGenerationPolicy.java +art/arcane/iris/platform/bukkit/nms/BukkitGeneratorContext.java +art/arcane/iris/platform/bukkit/nms/BukkitTileWriteScheduler.java +art/arcane/iris/platform/bukkit/nms/BukkitWorldLifecyclePolicy.java art/arcane/iris/platform/bukkit/nms/INMS.java art/arcane/iris/platform/bukkit/nms/INMSBinding.java art/arcane/iris/platform/bukkit/nms/MinecraftVersion.java @@ -88,17 +95,17 @@ art/arcane/iris/platform/bukkit/plugin/Chunks.java art/arcane/iris/platform/bukkit/plugin/IrisService.java art/arcane/iris/platform/bukkit/plugin/VolmitPlugin.java art/arcane/iris/platform/bukkit/plugin/VolmitSender.java -art/arcane/iris/platform/bukkit/registry/Attributes.java -art/arcane/iris/platform/bukkit/registry/KeyedRegistry.java -art/arcane/iris/platform/bukkit/registry/Materials.java -art/arcane/iris/platform/bukkit/registry/Particles.java -art/arcane/iris/platform/bukkit/registry/RegistryTypeAdapter.java -art/arcane/iris/platform/bukkit/registry/RegistryUtil.java art/arcane/iris/platform/generation/BukkitChunkGenerator.java art/arcane/iris/platform/generation/DummyBiomeProvider.java art/arcane/iris/platform/generation/DummyChunkGenerator.java art/arcane/iris/platform/generation/EngineBukkitOps.java art/arcane/iris/platform/reflect/KeyedType.java +art/arcane/iris/platform/registry/Attributes.java +art/arcane/iris/platform/registry/KeyedRegistry.java +art/arcane/iris/platform/registry/Materials.java +art/arcane/iris/platform/registry/Particles.java +art/arcane/iris/platform/registry/RegistryTypeAdapter.java +art/arcane/iris/platform/registry/RegistryUtil.java art/arcane/iris/structure/BulkStructureImporter.java art/arcane/iris/structure/FeatureImporter.java art/arcane/iris/structure/StructureCaptureImporter.java diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java b/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java index 36eb76536..6c408f217 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java @@ -323,6 +323,9 @@ final class DecoratorCore { int xf, int zf, int realX, int realZ, int height, int max, Hunk data, RNG rng, IrisData irisData, EngineMantle mantle) { + if (!hasFloatingSurface(data, xf, height, zf)) { + return; + } NativeBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ); if (bd == null) { return; @@ -367,6 +370,9 @@ final class DecoratorCore { int xf, int zf, int realX, int realZ, int height, int max, Hunk data, RNG rng, IrisData irisData, EngineMantle mantle) { + if (!hasFloatingSurface(data, xf, height, zf)) { + return 0; + } int stack = decorator.getHeight(rng, realX, realZ, irisData); if (decorator.isScaleStack()) { stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack()); @@ -413,6 +419,14 @@ final class DecoratorCore { return placed; } + private static boolean hasFloatingSurface(Hunk data, int x, int height, int z) { + if (height < 0 || height >= data.getHeight()) { + return false; + } + NativeBlockState surface = data.get(x, height, z); + return B.isSolid(surface); + } + static NativeBlockState fixFacesForHunk(NativeBlockState b, Hunk hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle) { if (!B.isVineBlock(b)) { diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/tree/IrisTreeProfile.java b/core/src/main/java/art/arcane/iris/generation/decoration/tree/IrisTreeProfile.java index cc1190e96..db1ab7481 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/tree/IrisTreeProfile.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/tree/IrisTreeProfile.java @@ -26,6 +26,8 @@ public enum IrisTreeProfile { OAK, @Description("Slim upright broadleaf crown.") BIRCH, + @Description("Upright broadleaf crown with a rounded upper canopy and a tapered top.") + POPLAR, @Description("Narrow conifer cone.") SPRUCE, @Description("Tall thin crown concentrated near the very top.") diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplier.java b/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplier.java index 2af5694ef..7ee57355e 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplier.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplier.java @@ -89,7 +89,7 @@ public final class TreeDecoratorApplier { private static void trunkSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - String[] facings = {"west", "east", "north", "south"}; + String[] facings = {"east", "west", "south", "north"}; for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) { for (int i = 0; i < sides.length; i++) { int nx = v.x() + sides[i][0]; @@ -104,7 +104,7 @@ public final class TreeDecoratorApplier { private static void leafSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) { int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - String[] facings = {"west", "east", "north", "south"}; + String[] facings = {"east", "west", "south", "north"}; for (TreeBlockCanvas.Vec v : new KList<>(canvas.getLeaf())) { for (int i = 0; i < sides.length; i++) { int nx = v.x() + sides[i][0]; diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeProfiles.java b/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeProfiles.java index 40b033060..16b7528de 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeProfiles.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/tree/TreeProfiles.java @@ -27,6 +27,7 @@ public final class TreeProfiles { return switch (profile) { case OAK, DARK_OAK, CHERRY -> 1.0; case BIRCH -> 0.7; + case POPLAR -> 0.8; case SPRUCE -> 0.55; case JUNGLE -> 0.45; case ACACIA -> 0.6; @@ -44,6 +45,7 @@ public final class TreeProfiles { return switch (profile) { case OAK -> new double[][]{{0.55, 0.6}, {0.65, 0.9}, {0.75, 1.0}, {0.85, 0.9}, {0.92, 0.65}, {0.98, 0.35}}; case BIRCH -> new double[][]{{0.6, 0.45}, {0.7, 0.75}, {0.8, 0.85}, {0.88, 0.75}, {0.94, 0.45}, {0.99, 0.2}}; + case POPLAR -> new double[][]{{0.5, 0.35}, {0.62, 0.7}, {0.74, 1.0}, {0.84, 0.95}, {0.93, 0.65}, {1.0, 0.3}}; case SPRUCE, MEGA_SPRUCE -> new double[][]{{0.3, 1.0}, {0.42, 0.85}, {0.54, 0.7}, {0.65, 0.55}, {0.75, 0.4}, {0.84, 0.25}, {0.91, 0.15}, {0.97, 0.05}}; case JUNGLE -> new double[][]{{0.82, 0.4}, {0.88, 0.8}, {0.93, 1.0}, {0.97, 0.7}, {1.0, 0.3}}; case ACACIA -> new double[][]{{0.9, 0.5}, {0.95, 0.8}, {0.99, 0.4}}; diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java index a78507822..f83b6ff91 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisPostModifier.java @@ -224,7 +224,7 @@ public class IrisPostModifier extends EngineAssignedModifier { } // Slab - if (slabs && !river) { + if (slabs && !river && isSolid(x, h, z, originX, originZ, currentData)) { //@builder if ((ha == h + 1 && isSolidNonSlab(x + 1, ha, z, originX, originZ, currentData)) || (hb == h + 1 && isSolidNonSlab(x, hb, z + 1, originX, originZ, currentData)) diff --git a/core/src/main/java/art/arcane/iris/localization/RuntimeUiMessages.java b/core/src/main/java/art/arcane/iris/localization/RuntimeUiMessages.java index e7e74b1f9..f6089a7dc 100644 --- a/core/src/main/java/art/arcane/iris/localization/RuntimeUiMessages.java +++ b/core/src/main/java/art/arcane/iris/localization/RuntimeUiMessages.java @@ -92,6 +92,7 @@ public final class RuntimeUiMessages { public static final TextKey DUST_COPY_BUTTON = TextKey.of("iris.runtime.dust.copy_button", "[Click to copy these stats]"); public static final TextKey DUST_COPY_HOVER = TextKey.of("iris.runtime.dust.copy_hover", "Copy block stats to clipboard"); public static final TextKey DUST_REVEAL_FAILED = TextKey.of("iris.runtime.dust.reveal_failed", "Object reveal failed; see the console for details."); + public static final TextKey DUST_BIOME_LOADING = TextKey.of("iris.runtime.dust.biome_loading", "Saved biome information is loading. Try revealing this block again shortly."); public static final TextKey WHAT_MATERIAL = TextKey.of("iris.runtime.what.material", "Material: {material}"); public static final TextKey WHAT_FULL_STATE = TextKey.of("iris.runtime.what.full_state", "Full: {state}"); public static final TextKey WHAT_ITEM_COUNT = TextKey.of("iris.runtime.what.item_count", "Count: {count}"); @@ -222,6 +223,7 @@ public final class RuntimeUiMessages { DUST_COPY_BUTTON, DUST_COPY_HOVER, DUST_REVEAL_FAILED, + DUST_BIOME_LOADING, WHAT_MATERIAL, WHAT_FULL_STATE, WHAT_ITEM_COUNT, diff --git a/core/src/main/java/art/arcane/iris/studio/edit/DustRevealer.java b/core/src/main/java/art/arcane/iris/studio/edit/DustRevealer.java index a9bd664f3..51fca1c25 100644 --- a/core/src/main/java/art/arcane/iris/studio/edit/DustRevealer.java +++ b/core/src/main/java/art/arcane/iris/studio/edit/DustRevealer.java @@ -36,6 +36,7 @@ import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.localization.MessageArgument; import art.arcane.iris.platform.bukkit.plugin.VolmitSender; import art.arcane.iris.world.task.J; +import art.arcane.iris.world.history.SavedBiomeUnavailableException; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.World; @@ -135,7 +136,16 @@ public class DustRevealer { Engine access = generator.getEngine(); if (access != null) { - describe(access, world, block, sender); + try { + describe(access, world, block, sender); + } catch (SavedBiomeUnavailableException unavailable) { + if (!unavailable.isLoading()) { + IrisLogging.reportError(unavailable); + } + sender.sendMessage(IrisLanguage.text(unavailable.isLoading() + ? RuntimeUiMessages.DUST_BIOME_LOADING : RuntimeUiMessages.DUST_REVEAL_FAILED)); + return; + } String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ()); if (a != null) { @@ -339,6 +349,8 @@ public class DustRevealer { private static T safe(Supplier supplier) { try { return supplier.get(); + } catch (SavedBiomeUnavailableException unavailable) { + throw unavailable; } catch (Throwable e) { IrisLogging.reportError(e); return null; diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index 13788a1c7..016735991 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -1378,6 +1378,7 @@ totals = "Gesamt: +{wood} Holz ({branches} Äste), {converted} Blätter->Holz, ~ position_set = "Position {position} auf {x}, {y}, {z} gesetzt" [iris.runtime.dust] +biome_loading = "Die gespeicherten Biominformationen werden geladen. Versuche in Kürze erneut, diesen Block zu untersuchen." iris_world_required = "Diese Dimension wurde nicht von Iris generiert." found_object = "Objekt {object} gefunden" revealed = "{count} Block/Blöcke von {object} aufgedeckt" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index 81b488d75..74785879a 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -1378,6 +1378,7 @@ totals = "Totales: +{wood} de madera ({branches} ramas), {converted} hojas->made position_set = "Posición {position} fijada en {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Se está cargando la información guardada del bioma. Vuelve a intentar revelar este bloque dentro de unos instantes." iris_world_required = "Esta dimensión no fue generada por Iris." found_object = "Objeto encontrado: {object}" revealed = "Se revelaron {count} bloque(s) de {object}" diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml index c466ef4c3..e361bf777 100644 --- a/core/src/main/resources/languages/fi_FI.toml +++ b/core/src/main/resources/languages/fi_FI.toml @@ -1383,6 +1383,7 @@ totals = "Yhteensä: +{wood} puu ({branches} konttorit), {converted} Lehti->puu{ position_set = "Sijainti {position} asetettu {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Tallennettuja biomitietoja ladataan. Yritä paljastaa tämän lohkon tiedot uudelleen hetken kuluttua." iris_world_required = "Iris ei ole luonut tätä ulottuvuutta." found_object = "Löytyi {object}" revealed = "{object}:n paljastamat {count}-katkokset" diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml index 8221ccf45..7a1a7c063 100644 --- a/core/src/main/resources/languages/fr_FR.toml +++ b/core/src/main/resources/languages/fr_FR.toml @@ -1378,6 +1378,7 @@ totals = "Totaux : +{wood} bois ({branches} branches), {converted} feuilles->boi position_set = "Position {position} définie sur {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Les informations enregistrées du biome sont en cours de chargement. Réessaie de révéler ce bloc dans quelques instants." iris_world_required = "Cette dimension n'a pas été générée par Iris." found_object = "Objet trouvé : {object}" revealed = "{count} bloc(s) de {object} révélés" diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml index 8590b4f5c..11c001580 100644 --- a/core/src/main/resources/languages/he_IL.toml +++ b/core/src/main/resources/languages/he_IL.toml @@ -1383,6 +1383,7 @@ totals = "סיכומים:+{wood} עץ ({branches} סניפים) {converted} הע position_set = "מיקום {position} להגדיר {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "נתוני הביומה השמורים נטענים. נסו לחשוף את הבלוק הזה שוב בעוד רגע." iris_world_required = "ממד זה לא נוצר על ידי Iris." found_object = "אובייקט {object}" revealed = "נחשף {count} בלוקים (s) {object}" diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml index 20442dc97..71d53a917 100644 --- a/core/src/main/resources/languages/it_IT.toml +++ b/core/src/main/resources/languages/it_IT.toml @@ -1378,6 +1378,7 @@ totals = "Totale: +{wood} legno ({branches} rami), {converted} foglie->legno, ~{ position_set = "Posizione {position} impostata a {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Caricamento delle informazioni salvate sul bioma in corso. Riprova a rivelare questo blocco tra qualche istante." iris_world_required = "Questa dimensione non è stata generata da Iris." found_object = "Oggetto trovato {object}" revealed = "Rivelato {count} blocco(i) di {object}" diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml index 5b2238192..29998c0d9 100644 --- a/core/src/main/resources/languages/ja-JP.toml +++ b/core/src/main/resources/languages/ja-JP.toml @@ -1381,6 +1381,7 @@ totals = "合計: 木材 +{wood}(枝 {branches})、葉 -> 木材 {converted} position_set = "選択位置 {position} を {x}, {y}, {z} に設定しました" [iris.runtime.dust] +biome_loading = "保存されたバイオーム情報を読み込んでいます。少し待ってから、このブロックの情報を再度確認してください。" iris_world_required = "このディメンションは Iris によって生成されていません。" found_object = "オブジェクト {object} が見つかりました" revealed = "{object} のブロックを {count} 個表示しました" diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml index 642f341b8..dcb44448c 100644 --- a/core/src/main/resources/languages/ko_KR.toml +++ b/core/src/main/resources/languages/ko_KR.toml @@ -1381,6 +1381,7 @@ totals = "합계: 나무 +{wood}개(가지 {branches}개), 잎->나무 {converte position_set = "선택 위치 {position}을(를) {x}, {y}, {z}(으)로 설정했습니다" [iris.runtime.dust] +biome_loading = "저장된 바이옴 정보를 불러오는 중입니다. 잠시 후 이 블록의 정보를 다시 확인해 주세요." iris_world_required = "이 차원은 Iris가 생성하지 않았습니다." found_object = "오브젝트 {object} 발견" revealed = "{object}의 블록 {count}개를 드러냈습니다" diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml index af191d941..ec28e1cb6 100644 --- a/core/src/main/resources/languages/lt_LT.toml +++ b/core/src/main/resources/languages/lt_LT.toml @@ -1383,6 +1383,7 @@ totals = "Iš viso: +{wood} medienos ({branches} filialai), {converted} Lapai->m position_set = "Padėtis {position} nustatyti {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Įkeliama išsaugota biomo informacija. Netrukus pabandyk dar kartą atskleisti šio bloko informaciją." iris_world_required = "Šio matmens nesugeneravo Iris." found_object = "Rastas objektas {object}" revealed = "Atšaldytas {count} blokas (-iai) {object}" diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml index ac4c3d4ac..a204dfa86 100644 --- a/core/src/main/resources/languages/nl_NL.toml +++ b/core/src/main/resources/languages/nl_NL.toml @@ -1378,6 +1378,7 @@ totals = "Totaal: +{wood} hout ({branches} takken), {converted} Bladeren->hout, position_set = "Positie {position} ingesteld op {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "De opgeslagen bioominformatie wordt geladen. Probeer dit blok over enkele ogenblikken opnieuw te onthullen." iris_world_required = "Deze dimensie is niet door Iris gegenereerd." found_object = "Object gevonden {object}" revealed = "Onthuld {count} blok(s) van {object}" diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml index c0954a64f..5936cc4ae 100644 --- a/core/src/main/resources/languages/pl_PL.toml +++ b/core/src/main/resources/languages/pl_PL.toml @@ -1383,6 +1383,7 @@ totals = "Suma: +{wood} drewno ({branches} ), {converted} liści->drewno, ~{dist position_set = "Pozycja {position} ustawiony na {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Trwa wczytywanie zapisanych informacji o biomie. Za chwilę spróbuj ponownie ujawnić informacje o tym bloku." iris_world_required = "Ten wymiar nie został wygenerowany przez Iris." found_object = "Znaleziono obiekt {object}" revealed = "Ujawnione {count} blok (-y) {object}" diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml index d5670c83d..5627c6c21 100644 --- a/core/src/main/resources/languages/pt_PT.toml +++ b/core/src/main/resources/languages/pt_PT.toml @@ -1378,6 +1378,7 @@ totals = "Total: +{wood} Madeira ({branches} ramos), {converted} folhas->madeira position_set = "Posição {position} definir para {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "As informações guardadas do bioma estão a ser carregadas. Tenta revelar este bloco novamente dentro de instantes." iris_world_required = "Esta dimensão não foi gerada pelo Iris." found_object = "Objeto encontrado {object}" revealed = "Revogado {count} bloco( s) de {object}" diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml index 9f9214829..f305bd5f1 100644 --- a/core/src/main/resources/languages/ru_RU.toml +++ b/core/src/main/resources/languages/ru_RU.toml @@ -1383,6 +1383,7 @@ totals = "Итоги: +{wood}древесина{branches}ветви,{converted} position_set = "Позиция {position} устанавливать {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Сохранённые сведения о биоме загружаются. Попробуйте снова раскрыть информацию об этом блоке через несколько секунд." iris_world_required = "Это измерение не было создано Iris." found_object = "Найден объект {object}" revealed = "раскрытый {count} блок(ы) {object}" diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml index da368bce2..5649ccba5 100644 --- a/core/src/main/resources/languages/tr_TR.toml +++ b/core/src/main/resources/languages/tr_TR.toml @@ -1383,6 +1383,7 @@ totals = "Toplamlar: +{wood} wood ({branches} şubeler) {converted} Yapraklar->A position_set = "Pozisyon pozisyonu {position} set toplan {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Kaydedilmiş biyom bilgileri yükleniyor. Bu bloğu açığa çıkarmayı kısa bir süre sonra tekrar deneyin." iris_world_required = "Bu boyut Iris tarafından oluşturulmadı." found_object = "bulundu object bulunamadı {object}" revealed = "Ortaya çıktı {count} blok (s) {object}" diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml index 17310c5f6..3c3b06cdb 100644 --- a/core/src/main/resources/languages/vi_VI.toml +++ b/core/src/main/resources/languages/vi_VI.toml @@ -1381,6 +1381,7 @@ totals = "Tổng: +{wood} gỗ ({branches} chi nhánh), {converted} để lại- position_set = "Vị trí {position} đặt {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "Đang tải thông tin quần xã sinh vật đã lưu. Hãy thử khám phá lại khối này sau giây lát." iris_world_required = "Chiều không gian này không được Iris tạo ra." found_object = "Đối tượng đã tìm {object}" revealed = "Đã tiết lộ{count}khối{object}" diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml index 22d81e30b..471381179 100644 --- a/core/src/main/resources/languages/zh_CN.toml +++ b/core/src/main/resources/languages/zh_CN.toml @@ -1381,6 +1381,7 @@ totals = "共计:+{wood} 木材({branches} 分部门), {converted} 叶->木头,~ position_set = "立场 {position} 设置为 {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "正在加载已保存的生物群系信息。请稍后再次尝试揭示此方块的信息。" iris_world_required = "此维度不是由 Iris 生成的。" found_object = "找到对象 {object}" revealed = "被揭露{count}区块{object}" diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml index fc1ebeafe..cb1ca9f36 100644 --- a/core/src/main/resources/languages/zh_TW.toml +++ b/core/src/main/resources/languages/zh_TW.toml @@ -1381,6 +1381,7 @@ totals = "共計:+{wood} 木材({branches} 分部門), {converted} 葉->木頭,~ position_set = "立場 {position} 設定為 {x}, {y}, {z}" [iris.runtime.dust] +biome_loading = "正在載入已儲存的生態域資訊。請稍後再次嘗試揭示此方塊的資訊。" iris_world_required = "此維度不是由 Iris 生成的。" found_object = "找到物件 {object}" revealed = "被揭露{count}區塊{object}" diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/DecoratorCoreTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/DecoratorCoreTest.java index 5e0557302..913b5b57e 100644 --- a/core/src/test/java/art/arcane/iris/generation/decoration/DecoratorCoreTest.java +++ b/core/src/test/java/art/arcane/iris/generation/decoration/DecoratorCoreTest.java @@ -298,6 +298,7 @@ public class DecoratorCoreTest { when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); Hunk output = Hunk.newArrayHunk(1, 2, 1); + output.set(0, 0, 0, sturdyState()); int placed = DecoratorCore.placeFloatingStacked( decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data, null); @@ -305,6 +306,72 @@ public class DecoratorCoreTest { assertSame(decorant, output.get(0, 1, 0)); } + @Test + public void floatingDecoratorsRejectUnsupportedSurface() { + IrisDecorator decorator = mock(IrisDecorator.class); + IrisData data = mock(IrisData.class); + NativeBlockState decorant = mock(NativeBlockState.class); + NativeBlockState air = airState(); + NativeBlockState fluid = mock(NativeBlockState.class); + NativeBlockState plant = mock(NativeBlockState.class); + when(plant.key()).thenReturn("minecraft:short_grass"); + when(fluid.isFluid()).thenReturn(true); + when(decorant.key()).thenReturn("minecraft:cobblestone_slab"); + when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(2); + when(decorator.getTopThreshold()).thenReturn(1.0); + when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); + when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); + + for (NativeBlockState surface : new NativeBlockState[]{null, air, fluid, plant}) { + Hunk output = Hunk.newArrayHunk(1, 5, 1); + output.set(0, 0, 0, sturdyState()); + output.set(0, 2, 0, surface); + output.set(0, 3, 0, air); + output.set(0, 4, 0, air); + + DecoratorCore.placeFloatingSimple( + decorator, 0, 0, 0, 0, 2, 3, output, new RNG(1L), data, null); + assertSame(air, output.get(0, 3, 0)); + + int placed = DecoratorCore.placeFloatingStacked( + decorator, 0, 0, 0, 0, 2, 3, output, new RNG(1L), data, null); + assertEquals(0, placed); + assertSame(surface, output.get(0, 2, 0)); + assertSame(air, output.get(0, 3, 0)); + assertSame(air, output.get(0, 4, 0)); + } + } + + @Test + public void floatingDecoratorsPreserveSupportedPlacementWithoutMaterialGating() { + IrisDecorator decorator = mock(IrisDecorator.class); + IrisData data = mock(IrisData.class); + NativeBlockState surface = mock(NativeBlockState.class); + NativeBlockState decorant = mock(NativeBlockState.class); + when(surface.isSolid()).thenReturn(true); + when(decorant.key()).thenReturn("minecraft:cobblestone_slab"); + when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(2); + when(decorator.getTopThreshold()).thenReturn(1.0); + when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); + when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant); + Hunk output = Hunk.newArrayHunk(1, 4, 1); + output.set(0, 1, 0, surface); + output.set(0, 2, 0, airState()); + output.set(0, 3, 0, airState()); + + DecoratorCore.placeFloatingSimple( + decorator, 0, 0, 0, 0, 1, 3, output, new RNG(1L), data, null); + assertSame(decorant, output.get(0, 2, 0)); + output.set(0, 2, 0, airState()); + + int placed = DecoratorCore.placeFloatingStacked( + decorator, 0, 0, 0, 0, 1, 3, output, new RNG(1L), data, null); + assertEquals(2, placed); + assertSame(surface, output.get(0, 1, 0)); + assertSame(decorant, output.get(0, 2, 0)); + assertSame(decorant, output.get(0, 3, 0)); + } + @Test public void tallSurfacePlantDoesNotPlaceWhenUpperTargetIsOccupied() { IrisDecorator decorator = mock(IrisDecorator.class); @@ -337,6 +404,7 @@ public class DecoratorCoreTest { when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant); Hunk output = Hunk.newArrayHunk(1, 4, 1); + output.set(0, 0, 0, sturdyState()); output.set(0, 1, 0, air); output.set(0, 2, 0, occupied); @@ -737,6 +805,7 @@ public class DecoratorCoreTest { private NativeBlockState sturdyState() { NativeBlockState support = mock(NativeBlockState.class); BlockData blockData = mock(BlockData.class); + when(support.isSolid()).thenReturn(true); when(support.nativeHandle()).thenReturn(blockData); when(blockData.isFaceSturdy(any(), eq(BlockSupport.FULL))).thenReturn(true); return support; diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/IrisSugarCanePlacementTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/IrisSugarCanePlacementTest.java index 0727e7ad8..88cc8ea4a 100644 --- a/core/src/test/java/art/arcane/iris/generation/decoration/IrisSugarCanePlacementTest.java +++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisSugarCanePlacementTest.java @@ -109,6 +109,8 @@ public class IrisSugarCanePlacementTest { local = size / 2; output = Hunk.newArrayHunk(size, 8, size); when(air.isAir()).thenReturn(true); + when(stone.isSolid()).thenReturn(true); + when(soil.isSolid()).thenReturn(true); when(water.isWater()).thenReturn(true); when(cane.canPlaceOnto(soil)).thenReturn(true); when(cane.canPlaceOnto(cane)).thenReturn(true); diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/tree/ProceduralTreeGeneratorTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/tree/ProceduralTreeGeneratorTest.java index 9e22e868f..28b13605c 100644 --- a/core/src/test/java/art/arcane/iris/generation/decoration/tree/ProceduralTreeGeneratorTest.java +++ b/core/src/test/java/art/arcane/iris/generation/decoration/tree/ProceduralTreeGeneratorTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import java.util.HashSet; import java.util.HashMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -88,6 +89,70 @@ public class ProceduralTreeGeneratorTest { assertTrue("leaves should rise above the trunk top", maxLeafY >= height - 1); } + @Test + public void poplarCrownIsBroaderAndHigherThanColumnarCrown() { + TreeBlockCanvas poplar = profileCanopy(IrisTreeProfile.POPLAR); + TreeBlockCanvas columnar = profileCanopy(IrisTreeProfile.COLUMNAR); + int poplarWidth = 0; + int columnarWidth = 0; + int poplarBottom = Integer.MAX_VALUE; + int columnarBottom = Integer.MAX_VALUE; + for (TreeBlockCanvas.Vec leaf : poplar.getLeaf()) { + poplarWidth = Math.max(poplarWidth, Math.abs(leaf.x())); + poplarBottom = Math.min(poplarBottom, leaf.y()); + } + for (TreeBlockCanvas.Vec leaf : columnar.getLeaf()) { + columnarWidth = Math.max(columnarWidth, Math.abs(leaf.x())); + columnarBottom = Math.min(columnarBottom, leaf.y()); + } + + assertTrue(poplarWidth > columnarWidth); + assertTrue(poplarBottom > columnarBottom); + } + + @Test + public void branchingPoplarVariantsRetainSupportedLeavesAndOrientedWood() { + IrisProceduralTree tree = new IrisProceduralTree() + .setProfile(IrisTreeProfile.POPLAR) + .setTrunk("minecraft:poplar_log") + .setLeaves("minecraft:orange_poplar_leaves") + .setLeanAngle(7); + IrisTreeBranches branches = new IrisTreeBranches() + .setProbabilityFunction(IrisTreeBranchProbability.TOP_HEAVY) + .setProbabilityExponent(2) + .setLengthFunction(IrisTreeFunction.CONSTANT) + .setLengthConstant(3) + .setElevation(35) + .setClusterRadius(2); + tree.getCanopy().setBranches(branches); + for (int height = 7; height <= 11; height++) { + TreeBlockCanvas canvas = new TreeBlockCanvas(); + List endpoints = new ArrayList<>(); + double[][] offsets = TreeTrunkBuilder.build(canvas, tree, height).getFirst().offsets(); + TreeCanopyBuilder.build(canvas, tree, height, offsets, 0, height * 7919L, endpoints); + TreeSupport.ensureLeavesSupported(canvas, 24); + + assertFalse(endpoints.isEmpty()); + assertFalse(canvas.getLeaf().isEmpty()); + Map distances = TreePlausibility.computeDistances(canvas.getTrunk(), canvas.getLeaf()); + for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) { + assertNotNull(distances.get(leaf)); + assertTrue(distances.get(leaf) <= 6); + } + for (TreeBlockCanvas.Vec wood : canvas.getTrunk()) { + assertTrue(canvas.get(wood.x(), wood.y(), wood.z()).axis() != TreeBlockCanvas.Axis.NONE); + } + } + } + + private static TreeBlockCanvas profileCanopy(IrisTreeProfile profile) { + IrisProceduralTree tree = new IrisProceduralTree().setProfile(profile); + TreeBlockCanvas canvas = new TreeBlockCanvas(); + double[][] offsets = TreeTrunkBuilder.build(canvas, tree, 14).getFirst().offsets(); + TreeCanopyBuilder.build(canvas, tree, 14, offsets, 0, 1234L, null); + return canvas; + } + @Test public void hollowCanopyHasFewerLeavesThanFilled() { TreeBlockCanvas filled = canopyOnly(IrisTreeLeafMode.FILLED); diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplierTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplierTest.java index 3c818c29b..4baf53acf 100644 --- a/core/src/test/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplierTest.java +++ b/core/src/test/java/art/arcane/iris/generation/decoration/tree/TreeDecoratorApplierTest.java @@ -6,8 +6,46 @@ import org.junit.Test; import java.util.List; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class TreeDecoratorApplierTest { + @Test + public void trunkAttachmentsFaceAwayFromTheirWoodSupport() { + TreeBlockCanvas canvas = new TreeBlockCanvas(); + canvas.setTrunk(0, 2, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.Y); + IrisProceduralTree tree = surfaceTree(IrisTreeDecoratorTarget.TRUNK_SURFACE, true); + + TreeDecoratorApplier.apply(canvas, tree, 918L, List.of()); + + assertOutwardFacings(canvas); + assertEquals(1, canvas.getTrunk().size()); + assertEquals(5, canvas.getCells().size()); + } + + @Test + public void leafAttachmentsFaceAwayFromTheirLeafSupport() { + TreeBlockCanvas canvas = new TreeBlockCanvas(); + canvas.setLeaf(0, 2, 0, TreeBlockCanvas.Role.LEAF); + IrisProceduralTree tree = surfaceTree(IrisTreeDecoratorTarget.LEAF_SURFACE, true); + + TreeDecoratorApplier.apply(canvas, tree, 918L, List.of()); + + assertOutwardFacings(canvas); + assertEquals(1, canvas.getLeaf().size()); + } + + @Test + public void trunkAttachmentsKeepExplicitBlockFacingWhenAxisAwarenessIsDisabled() { + TreeBlockCanvas canvas = new TreeBlockCanvas(); + canvas.setTrunk(0, 2, 0, TreeBlockCanvas.Role.TRUNK, TreeBlockCanvas.Axis.Y); + IrisProceduralTree tree = surfaceTree(IrisTreeDecoratorTarget.TRUNK_SURFACE, false); + + TreeDecoratorApplier.apply(canvas, tree, 918L, List.of()); + + assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(1, 2, 0).role()); + assertNull(canvas.get(1, 2, 0).facing()); + } + @Test public void branchTipsExtendOutwardThroughLeavesWithoutReplacingWood() { TreeBlockCanvas canvas = new TreeBlockCanvas(); @@ -39,4 +77,20 @@ public class TreeDecoratorApplierTest { assertEquals(11, canvas.getCells().size()); } + + private static IrisProceduralTree surfaceTree(IrisTreeDecoratorTarget target, boolean axisAware) { + IrisTreeDecorator decorator = new IrisTreeDecorator() + .setTarget(target) + .setBlock("minecraft:shelf_mushroom") + .setChance(1D) + .setAxisAware(axisAware); + return new IrisProceduralTree().setDecorators(new KList<>(decorator)); + } + + private static void assertOutwardFacings(TreeBlockCanvas canvas) { + assertEquals("east", canvas.get(1, 2, 0).facing()); + assertEquals("west", canvas.get(-1, 2, 0).facing()); + assertEquals("south", canvas.get(0, 2, 1).facing()); + assertEquals("north", canvas.get(0, 2, -1).facing()); + } } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java new file mode 100644 index 000000000..7a635f226 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPostModifierSlabSupportTest.java @@ -0,0 +1,109 @@ +package art.arcane.iris.generation.stage; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomePaletteLayer; +import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.context.ChunkedDataCache; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.terrain.IrisSlopeClip; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.stream.ProceduralStream; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; + +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class IrisPostModifierSlabSupportTest { + private static final int SURFACE = 12; + private static final int PLANE_WIDTH = 5; + private static final NativeBlockState AIR = state("minecraft:air", false, true, false); + private static final NativeBlockState WATER = state("minecraft:water", false, false, true); + private static final NativeBlockState STONE = state("minecraft:stone", true, false, false); + private static final NativeBlockState SLAB = state("minecraft:cobblestone_slab", true, false, false); + + @Test + public void supportedStepReceivesSmoothingBlock() throws ReflectiveOperationException { + assertSame(SLAB, smoothedBlock(STONE)); + } + + @Test + public void carvedAwaySurfaceDoesNotReceiveFloatingSmoothingBlock() throws ReflectiveOperationException { + assertSame(AIR, smoothedBlock(AIR)); + } + + @Test + public void fluidSurfaceDoesNotSupportSmoothingBlock() throws ReflectiveOperationException { + assertSame(AIR, smoothedBlock(WATER)); + } + + @SuppressWarnings("unchecked") + private NativeBlockState smoothedBlock(NativeBlockState support) throws ReflectiveOperationException { + Hunk output = Hunk.newHunk(3, 24, 3); + for (int x = 0; x < 3; x++) { + for (int z = 0; z < 3; z++) { + for (int y = 0; y < output.getHeight(); y++) { + output.set(x, y, z, y <= SURFACE ? STONE : AIR); + } + } + } + output.set(1, SURFACE, 1, support); + output.set(2, SURFACE + 1, 1, STONE); + + IrisBiomePaletteLayer slabLayer = mock(IrisBiomePaletteLayer.class); + doReturn(new IrisSlopeClip()).when(slabLayer).getSlopeCondition(); + doReturn(SLAB).when(slabLayer).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any()); + IrisBiome biome = mock(IrisBiome.class); + doReturn(slabLayer).when(biome).getSlab(); + ChunkedDataCache biomes = mock(ChunkedDataCache.class); + doReturn(biome).when(biomes).get(1, 1); + ChunkContext context = mock(ChunkContext.class); + doReturn(biomes).when(context).getBiome(); + + ProceduralStream zeroStream = mock(ProceduralStream.class); + doReturn(0D).when(zeroStream).get(anyDouble(), anyDouble()); + IrisComplex complex = mock(IrisComplex.class); + doReturn(zeroStream).when(complex).getRiverCarveWeightStream(); + doReturn(zeroStream).when(complex).getRiverWaterSurfaceStream(); + doReturn(zeroStream).when(complex).getSlopeStream(); + Engine engine = mock(Engine.class); + doReturn(complex).when(engine).getComplex(); + doReturn(mock(IrisData.class)).when(engine).getData(); + + IrisPostModifier modifier = mock(IrisPostModifier.class, CALLS_REAL_METHODS); + doReturn(engine).when(modifier).getEngine(); + Field rng = IrisPostModifier.class.getDeclaredField("rng"); + rng.setAccessible(true); + rng.set(modifier, new RNG(88L)); + int[] heights = new int[PLANE_WIDTH * PLANE_WIDTH]; + Arrays.fill(heights, SURFACE); + heights[2 * PLANE_WIDTH + 3] = SURFACE + 1; + + Method post = IrisPostModifier.class.getDeclaredMethod("post", int.class, int.class, Hunk.class, + int.class, int.class, ChunkContext.class, int[].class, int.class, boolean.class, boolean.class); + post.setAccessible(true); + post.invoke(modifier, 1, 1, output, 1, 1, context, heights, PLANE_WIDTH, false, true); + return output.get(1, SURFACE + 1, 1); + } + + private static NativeBlockState state(String key, boolean solid, boolean air, boolean fluid) { + NativeBlockState state = mock(NativeBlockState.class); + doReturn(key).when(state).key(); + doReturn(key).when(state).materialKey(); + doReturn(solid).when(state).isSolid(); + doReturn(air).when(state).isAir(); + doReturn(fluid).when(state).isFluid(); + return state; + } +} diff --git a/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java b/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java index b5c6ba69c..fb3978534 100644 --- a/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java +++ b/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java @@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest { @ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); - private static final String EXPECTED_SCHEMA_DIGEST = "91c36f3cdb0a10a0700b62fb6a6d2456576bd50f75a4d588103acdd42a7763c1"; + private static final String EXPECTED_SCHEMA_DIGEST = "7950fa5a3f3abf7fdd2cea1bd4b1fd335745b348ce64e67404cc86e898d18f16"; private static final List> SCHEMA_ROOTS = List.of( IrisDimension.class, diff --git a/core/src/test/java/art/arcane/iris/studio/edit/DustRevealerTest.java b/core/src/test/java/art/arcane/iris/studio/edit/DustRevealerTest.java new file mode 100644 index 000000000..1dbad603b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/studio/edit/DustRevealerTest.java @@ -0,0 +1,150 @@ +package art.arcane.iris.studio.edit; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.localization.IrisLanguage; +import art.arcane.iris.localization.RuntimeUiMessages; +import art.arcane.iris.platform.bukkit.nms.INMS; +import art.arcane.iris.platform.bukkit.nms.INMSBinding; +import art.arcane.iris.platform.bukkit.plugin.VolmitSender; +import art.arcane.iris.platform.generation.PlatformChunkGenerator; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.world.IrisToolbelt; +import art.arcane.iris.world.history.SavedBiomeUnavailableException; +import art.arcane.iris.world.task.J; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class DustRevealerTest { + @Test + public void loadingSurfaceBiomeSendsOneNoticeAndStopsTheReveal() { + try (Fixture fixture = new Fixture()) { + when(fixture.engine.getSurfaceBiome(192, 816)) + .thenThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true)); + + DustRevealer.spawn(fixture.block, fixture.sender); + + verify(fixture.sender).sendMessage(IrisLanguage.text(RuntimeUiMessages.DUST_BIOME_LOADING)); + verifyNoMoreInteractions(fixture.sender); + verify(fixture.engine).getObjectPlacementKey(192, 96, 816); + fixture.verifyNoLaterQueriesOrReveal(); + fixture.logging.verifyNoInteractions(); + } + } + + @Test + public void loadingDuringHeightLookupSendsOneNoticeBeforeObjectLookup() { + try (Fixture fixture = new Fixture()) { + when(fixture.engine.getHeight(192, 816, true)) + .thenThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true)); + + DustRevealer.spawn(fixture.block, fixture.sender); + + verify(fixture.sender).sendMessage(IrisLanguage.text(RuntimeUiMessages.DUST_BIOME_LOADING)); + verifyNoMoreInteractions(fixture.sender); + verify(fixture.engine, never()).getObjectPlacementKey(anyInt(), anyInt(), anyInt()); + fixture.verifyNoLaterQueriesOrReveal(); + fixture.logging.verifyNoInteractions(); + } + } + + @Test + public void anotherClickReportsTheBiomeAfterLoadingFinishes() { + try (Fixture fixture = new Fixture()) { + IrisBiome biome = mock(IrisBiome.class); + when(biome.getLoadKey()).thenReturn("ready-biome"); + when(fixture.engine.getSurfaceBiome(192, 816)) + .thenThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true)) + .thenReturn(biome); + + DustRevealer.spawn(fixture.block, fixture.sender); + verify(fixture.sender).sendMessage(IrisLanguage.text(RuntimeUiMessages.DUST_BIOME_LOADING)); + clearInvocations(fixture.sender); + + DustRevealer.spawn(fixture.block, fixture.sender); + + ArgumentCaptor messages = ArgumentCaptor.forClass(String.class); + verify(fixture.sender, atLeastOnce()).sendMessage(messages.capture()); + assertTrue(messages.getAllValues().stream().anyMatch(message -> message.contains("ready-biome"))); + verify(fixture.sender, never()).sendMessage(IrisLanguage.text(RuntimeUiMessages.DUST_BIOME_LOADING)); + verify(fixture.sender, never()).sendMessage(IrisLanguage.text(RuntimeUiMessages.DUST_REVEAL_FAILED)); + fixture.logging.verifyNoInteractions(); + } + } + + @Test + public void unavailableSavedBiomeReportsTheOriginalFailureAndStopsTheReveal() { + try (Fixture fixture = new Fixture()) { + SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException( + "Saved biome data could not be read.", new IllegalStateException("Corrupt biome receipt")); + when(fixture.engine.getSurfaceBiome(192, 816)).thenThrow(failure); + + DustRevealer.spawn(fixture.block, fixture.sender); + + fixture.logging.verify(() -> IrisLogging.reportError(failure)); + fixture.logging.verifyNoMoreInteractions(); + verify(fixture.sender).sendMessage(IrisLanguage.text(RuntimeUiMessages.DUST_REVEAL_FAILED)); + verifyNoMoreInteractions(fixture.sender); + verify(fixture.engine).getObjectPlacementKey(192, 96, 816); + fixture.verifyNoLaterQueriesOrReveal(); + } + } + + private static final class Fixture implements AutoCloseable { + private final MockedStatic toolbelt = mockStatic(IrisToolbelt.class); + private final MockedStatic logging = mockStatic(IrisLogging.class); + private final MockedStatic nativeAccess = mockStatic(INMS.class); + private final MockedStatic scheduling = mockStatic(J.class); + private final Engine engine = mock(Engine.class); + private final World world = mock(World.class); + private final Block block = mock(Block.class); + private final VolmitSender sender = mock(VolmitSender.class); + + private Fixture() { + PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class); + when(generator.getEngine()).thenReturn(engine); + toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator); + INMSBinding binding = mock(INMSBinding.class); + when(binding.getTrueBiomeBaseKey(any())).thenReturn("minecraft:plains"); + nativeAccess.when(INMS::get).thenReturn(binding); + when(block.getWorld()).thenReturn(world); + when(block.getX()).thenReturn(192); + when(block.getY()).thenReturn(96); + when(block.getZ()).thenReturn(816); + when(block.getType()).thenReturn(Material.STONE); + when(engine.getHeight(192, 816, true)).thenReturn(96); + } + + private void verifyNoLaterQueriesOrReveal() { + verify(engine, never()).getBiome(anyInt(), anyInt(), anyInt()); + verify(engine, never()).getCaveOrMantleBiome(anyInt(), anyInt(), anyInt()); + verify(engine, never()).getRegion(anyInt(), anyInt()); + verify(engine, never()).getObjectsAt(anyInt(), anyInt()); + scheduling.verifyNoInteractions(); + } + + @Override + public void close() { + scheduling.close(); + nativeAccess.close(); + logging.close(); + toolbelt.close(); + } + } +} diff --git a/gradle.properties b/gradle.properties index acbf85722..3c685a841 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,4 +28,4 @@ apiVersion=26.1 fabricLoaderVersion=0.19.3 forgeVersion=26.2-65.1.1 neoForgeVersion=26.2.0.59 -volmLibCoordinate=com.github.VolmitSoftware.VolmLib:volmlib-shared:6c54bbcd19282508364794df1eacb9720d94a896 +volmLibCoordinate=com.github.VolmitSoftware.VolmLib:volmlib-shared:d24d28acddbf3555c0c30ac99569ebacd476ce27