diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java index b117d354c..2a49e1344 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java @@ -70,10 +70,10 @@ public class CustomBiomeSource extends BiomeSource { LinkedHashSet> biomes = new LinkedHashSet<>(); for (IrisBiome i : engine.getAllBiomes()) { - Holder vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative()); + Holder vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey()); if (vanillaHolder == null) { throw new IllegalStateException("Iris structure biome derivative '" - + i.getVanillaDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'"); + + i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'"); } biomes.add(vanillaHolder); @@ -312,10 +312,10 @@ public class CustomBiomeSource extends BiomeSource { throw new IllegalStateException("Iris returned no surface structure biome at block " + blockX + "," + blockZ); } - Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative()); + Holder holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey()); if (holder == null) { throw new IllegalStateException("Iris structure biome derivative '" - + irisBiome.getVanillaDerivativeKey() + "' is not registered at block " + + irisBiome.getStructureDerivativeKey() + "' is not registered at block " + blockX + "," + blockZ); } return holder; @@ -371,10 +371,11 @@ public class CustomBiomeSource extends BiomeSource { + x + "," + y + "," + z); } - Holder holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative()); + Holder holder = resolveBiomeHolder( + biomeRegistry, resolution.irisBiome.getStructureDerivativeKey()); if (holder == null) { throw new IllegalStateException("Iris structure biome derivative '" - + resolution.irisBiome.getVanillaDerivativeKey() + "' is not registered at block " + + resolution.irisBiome.getStructureDerivativeKey() + "' is not registered at block " + resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ); } return holder; @@ -505,6 +506,17 @@ public class CustomBiomeSource extends BiomeSource { return optionalReferenceHolder.get(); } + private static Holder resolveBiomeHolder(Registry registry, String biomeKey) { + if (registry == null || biomeKey == null || biomeKey.isBlank()) { + return null; + } + Identifier identifier = Identifier.tryParse(biomeKey); + if (identifier == null) { + return null; + } + return registry.get(ResourceKey.create(Registries.BIOME, identifier)).orElse(null); + } + private static Holder resolveFallbackBiome(Registry registry, Registry customRegistry) { Holder plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS); if (plains != null) { diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java index b083b4be6..7ff5cfde2 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java @@ -3,7 +3,6 @@ package art.arcane.iris.core.nms.v26_2_R1; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; -import art.arcane.iris.core.structure.NativeStructureLocateCapability; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisNativeStructureDecision; import art.arcane.iris.engine.object.IrisStructureStiltSettings; @@ -174,7 +173,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { String key = id.toString(); IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step())); - if (NativeStructureLocateCapability.isPaperUnavailable(key) || !decision.generate()) { + if (!decision.generate()) { continue; } candidates.add(new NativeLocateCandidate(holder, key)); diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java new file mode 100644 index 000000000..dccc475e7 --- /dev/null +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java @@ -0,0 +1,22 @@ +package art.arcane.iris.core.nms.v26_2_R1; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CustomBiomeSourceStructureContractTest { + @Test + public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException { + String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource"))); + + assertTrue(source.contains("resolveBiomeHolder(registry, i.getStructureDerivativeKey())")); + assertTrue(source.contains("resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey())")); + assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()")); + assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()")); + } +} diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java index d5eeceebe..a27289b63 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGeneratorMonumentLocateContractTest.java @@ -17,7 +17,7 @@ import static org.junit.Assert.assertSame; public class IrisChunkGeneratorMonumentLocateContractTest { @Test - public void nativeLocateAllowsExplicitReplacementBeforeNativeCapabilityGate() throws IOException { + public void nativeLocateAllowsMonumentsAfterPolicyAndReachabilityChecks() throws IOException { String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))); int findStart = source.indexOf("findNearestMapStructure(ServerLevel level"); assertTrue(findStart >= 0); @@ -42,9 +42,9 @@ public class IrisChunkGeneratorMonumentLocateContractTest { int reachabilityStart = source.indexOf("private Set reachableStructureKeys", filterStart); assertTrue(reachabilityStart > filterStart); String filterMethod = source.substring(filterStart, reachabilityStart); - int monumentReject = filterMethod.indexOf("if (NativeStructureLocateCapability.isPaperUnavailable(key)"); - int rejectContinue = filterMethod.indexOf("continue;", monumentReject); - int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", rejectContinue); + int policyFilter = filterMethod.indexOf("if (!decision.generate())"); + int filterContinue = filterMethod.indexOf("continue;", policyFilter); + int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", filterContinue); int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition); assertTrue(policyResolution >= 0); @@ -58,10 +58,11 @@ public class IrisChunkGeneratorMonumentLocateContractTest { assertTrue(nativeFilter >= 0); assertTrue(delegateLocate > nativeFilter); assertTrue(nearestSelection > delegateLocate); - assertTrue(monumentReject >= 0); - assertTrue(rejectContinue > monumentReject); - assertTrue(emptyNativePartition > rejectContinue); + assertTrue(policyFilter >= 0); + assertTrue(filterContinue > policyFilter); + assertTrue(emptyNativePartition > filterContinue); assertTrue(reachabilityLookup > emptyNativePartition); + assertFalse(filterMethod.contains("NativeStructureLocateCapability")); assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)")); assertTrue(irisHelper.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())")); } 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 a9cac52d3..9fee4a180 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 @@ -173,6 +173,24 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0)); } + @Test + public void raisedSurfaceTerrainDoesNotSliceTreeBlocks() { + Map blocks = new HashMap<>(); + BlockState log = Blocks.OAK_LOG.defaultBlockState(); + BlockState leaves = Blocks.OAK_LEAVES.defaultBlockState(); + put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState()); + put(blocks, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState()); + put(blocks, 0, 66, 0, log); + put(blocks, 0, 68, 0, leaves); + + NativeStructurePostProcessor.applySurfaceColumn( + world(blocks), new BlockPos.MutableBlockPos(), + 0, 0, 64, 68, -64, 319); + + assertEquals(log, state(blocks, 0, 66, 0)); + assertEquals(leaves, state(blocks, 0, 68, 0)); + } + @Test public void loweredFluidColumnsRemainFluidFilled() { Map blocks = new HashMap<>(); @@ -266,11 +284,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest { } @Test - public void vegetationCleanupUsesTheSameCircularTaperAsTerrain() { - BoundingBox piece = new BoundingBox(0, 60, 0, 4, 80, 4); + public void templateAirDoesNotEraseTreesInsideVillagePieces() throws Exception { + BlockPos origin = new BlockPos(0, 80, 0); + StructureTemplate template = template(List.of( + new StructureTemplate.StructureBlockInfo(BlockPos.ZERO, Blocks.AIR.defaultBlockState(), null))); + StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.NONE); + Map blocks = new HashMap<>(); + BlockState log = Blocks.OAK_LOG.defaultBlockState(); + blocks.put(origin, log); - assertTrue(NativeStructurePostProcessor.withinSurfaceTerrainRadius(16, 2, piece, 12)); - assertFalse(NativeStructurePostProcessor.withinSurfaceTerrainRadius(16, 16, piece, 12)); + NativeStructurePostProcessor.clearTemplateAir(world(blocks), template, origin, 80, settings); + + assertEquals(log, blocks.get(origin)); } private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) { 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 0f3bbe51b..82078552d 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 @@ -27,8 +27,8 @@ public class NativeStructurePostProcessorVegetationTest { } @Test - public void surfaceStructuresClearTheirEntireFootprintByDefault() { - assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( + public void surfaceStructuresPreserveVegetationUnlessConfigured() { + assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( GenerationStep.Decoration.SURFACE_STRUCTURES, false)); assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint( GenerationStep.Decoration.SURFACE_STRUCTURES, true)); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java index d71652ef2..0ae93436e 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -21,7 +21,6 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.core.service.ObjectStudioSaveService; -import art.arcane.iris.core.structure.NativeStructureLocateCapability; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; @@ -136,10 +135,6 @@ public class CommandFind implements DirectorExecutor { locateIrisStructure(e, structureKey, commandSender); return; } - if (NativeStructureLocateCapability.isPaperUnavailable(structureKey)) { - commandSender.sendMessage(C.RED + NativeStructureLocateCapability.unavailableMessage()); - return; - } } if (nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)) { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java index a5642ebb4..cf9d9de2d 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java @@ -21,7 +21,6 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.structure.BulkStructureImporter; -import art.arcane.iris.core.structure.NativeStructureLocateCapability; import art.arcane.iris.core.structure.StructureCaptureImporter; import art.arcane.iris.core.structure.StructureImporter; import art.arcane.iris.core.structure.StructureIndexService; @@ -185,7 +184,6 @@ public class CommandStructure implements DirectorExecutor { int nativeEligible = 0; int disabled = 0; int unreachable = 0; - int unavailable = 0; int searchLimited = 0; int errors = 0; for (String keyName : structureKeys) { @@ -228,19 +226,12 @@ public class CommandStructure implements DirectorExecutor { continue; } nativeEligible++; - if (NativeStructureLocateCapability.isPaperUnavailable(keyName)) { - unavailable++; - messages.add(C.YELLOW + "[native-eligible/locate-unavailable] " + C.WHITE + keyName + C.YELLOW + ": " - + NativeStructureLocateCapability.unavailableMessage()); - continue; - } messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName); } messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, " + C.WHITE + nativeEligible + C.GREEN + " native structures eligible, " + C.WHITE + disabled + C.GREEN + " disabled by policy, " + C.WHITE + unreachable + C.GREEN + " biome-unreachable, " - + C.WHITE + unavailable + C.GREEN + " generation-eligible but unavailable to synchronous locate, " + C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, " + C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates."); sendVerificationMessages(commandSender, target, messages); diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java index 500dc2fd2..41284811e 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/IrisStructureLocateCommandContractTest.java @@ -35,18 +35,17 @@ public class IrisStructureLocateCommandContractTest { int replacementCheck = method.indexOf( "decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution); int replacementLocate = method.indexOf("locateIrisStructure(e, structureKey, commandSender)", replacementCheck); - int capabilityCheck = method.indexOf("NativeStructureLocateCapability.isPaperUnavailable(structureKey)", replacementLocate); int genericIrisLookup = method.indexOf( - "nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)", capabilityCheck); + "nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)", replacementLocate); int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", genericIrisLookup); assertTrue(nativeResolution >= 0); assertTrue(policyResolution > nativeResolution); assertTrue(replacementCheck > policyResolution); assertTrue(replacementLocate > replacementCheck); - assertTrue(capabilityCheck > replacementLocate); - assertTrue(genericIrisLookup > capabilityCheck); + assertTrue(genericIrisLookup > replacementLocate); assertTrue(nativeLocate > policyResolution); assertTrue(method.contains("decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS")); + assertFalse(method.contains("NativeStructureLocateCapability")); } @Test diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java index 391db4f2c..285d8ef04 100644 --- a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java @@ -7,6 +7,7 @@ import art.arcane.iris.engine.object.IrisStructureStiltSettings; import art.arcane.volmlib.util.math.RNG; import com.mojang.datafixers.util.Either; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.Identifier; import net.minecraft.tags.BlockTags; import net.minecraft.world.level.ChunkPos; @@ -252,7 +253,7 @@ public final class NativeStructurePostProcessor { public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step, boolean configured) { - return configured || !isUndergroundStep(step); + return configured; } public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area, @@ -433,9 +434,15 @@ public final class NativeStructurePostProcessor { return; } for (int y = originalY + 1; y < targetY; y++) { - world.setBlock(position.set(x, y, z), materials.subsurface(), 2); + position.set(x, y, z); + if (!isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.subsurface(), 2); + } + } + position.set(x, targetY, z); + if (!isTreeBlock(world.getBlockState(position))) { + world.setBlock(position, materials.surface(), 2); } - world.setBlock(position.set(x, targetY, z), materials.surface(), 2); } private static BlockState clearSurfaceDecorationAndResolveFill( @@ -531,8 +538,10 @@ public final class NativeStructurePostProcessor { BlockState air = Blocks.AIR.defaultBlockState(); for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) { BlockPos airPosition = airBlock.pos(); + BlockState existingState = world.getBlockState(airPosition); if (shouldClearLegacyAir( - airPosition.getY(), groundY, world.getBlockState(airPosition).isAir())) { + airPosition.getY(), groundY, existingState.isAir()) + && !isTreeBlock(existingState)) { world.setBlock(airPosition, air, 2); } } @@ -620,7 +629,7 @@ public final class NativeStructurePostProcessor { } boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()]; for (VegetationTarget target : targets) { - if (target != null && target.start() != null && target.start().isValid()) { + if (target != null && target.force() && target.start() != null && target.start().isValid()) { markVegetationColumns(area, snapshot, target, clearColumns); } } @@ -673,25 +682,19 @@ public final class NativeStructurePostProcessor { private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot, VegetationTarget target, boolean[] clearColumns) { int width = area.getXSpan(); - int padding = requiresSurfaceTerrain(target.start()) ? SURFACE_TERRAIN_RADIUS : 0; int[] pieceTops = new int[clearColumns.length]; Arrays.fill(pieceTops, Integer.MIN_VALUE); for (StructurePiece piece : target.start().getPieces()) { BoundingBox bounds = piece.getBoundingBox(); - int minX = Math.max(area.minX(), bounds.minX() - padding); - int maxX = Math.min(area.maxX(), bounds.maxX() + padding); - int minZ = Math.max(area.minZ(), bounds.minZ() - padding); - int maxZ = Math.min(area.maxZ(), bounds.maxZ() + padding); + int minX = Math.max(area.minX(), bounds.minX()); + int maxX = Math.min(area.maxX(), bounds.maxX()); + int minZ = Math.max(area.minZ(), bounds.minZ()); + int maxZ = Math.min(area.maxZ(), bounds.maxZ()); if (minX > maxX || minZ > maxZ) { continue; } for (int z = minZ; z <= maxZ; z++) { for (int x = minX; x <= maxX; x++) { - if (padding > 0) { - if (!withinSurfaceTerrainRadius(x, z, bounds, padding)) { - continue; - } - } int column = (z - area.minZ()) * width + x - area.minX(); pieceTops[column] = Math.max(pieceTops[column], bounds.maxY()); } @@ -707,12 +710,6 @@ public final class NativeStructurePostProcessor { } } - static boolean withinSurfaceTerrainRadius(int x, int z, BoundingBox bounds, int radius) { - int outX = IrisObjectVacuum.outset(x, bounds.minX(), bounds.maxX()); - int outZ = IrisObjectVacuum.outset(z, bounds.minZ(), bounds.maxZ()); - return (long) outX * outX + (long) outZ * outZ <= (long) radius * radius; - } - static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) { return force || pieceTopY >= lowestTreeY; } @@ -742,7 +739,13 @@ public final class NativeStructurePostProcessor { } private static boolean isTreeBlock(BlockState state) { - return state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES); + if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) { + return true; + } + String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath(); + return path.endsWith("_log") || path.endsWith("_wood") + || path.endsWith("_stem") || path.endsWith("_hyphae") + || path.endsWith("_leaves"); } private static List foundationEnvelope(BoundingBox area, StructureStart start) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java index ffd88328c..ea09223e1 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedBiomeSource.java @@ -242,10 +242,10 @@ final class IrisModdedBiomeSource extends BiomeSource { throw new IllegalStateException("Iris returned no surface structure biome at quart " + quartX + "," + quartZ); } - Holder resolved = resolveHolder(registry, irisBiome.getVanillaDerivativeKey()); + Holder resolved = resolveHolder(registry, irisBiome.getStructureDerivativeKey()); if (resolved == null) { throw new IllegalStateException("Iris structure biome derivative '" - + irisBiome.getVanillaDerivativeKey() + "' is not registered at quart " + + irisBiome.getStructureDerivativeKey() + "' is not registered at quart " + quartX + "," + quartZ); } return resolved; @@ -262,10 +262,10 @@ final class IrisModdedBiomeSource extends BiomeSource { if (registry == null) { throw new IllegalStateException("Iris structure biome lookup has no biome registry"); } - Holder resolved = resolveHolder(registry, resolution.irisBiome().getVanillaDerivativeKey()); + Holder resolved = resolveHolder(registry, resolution.irisBiome().getStructureDerivativeKey()); if (resolved == null) { throw new IllegalStateException("Iris structure biome derivative '" - + resolution.irisBiome().getVanillaDerivativeKey() + "' is not registered at block " + + resolution.irisBiome().getStructureDerivativeKey() + "' is not registered at block " + resolution.blockX() + "," + resolution.blockY() + "," + resolution.blockZ()); } return resolved; @@ -353,7 +353,7 @@ final class IrisModdedBiomeSource extends BiomeSource { throw new IllegalStateException("Iris returned no structure biome at quart " + quartX + "," + quartY + "," + quartZ); } - String derivativeKey = irisBiome.getVanillaDerivativeKey(); + String derivativeKey = irisBiome.getStructureDerivativeKey(); Holder resolved = resolveHolder(registry, derivativeKey); if (resolved == null) { throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey @@ -435,7 +435,7 @@ final class IrisModdedBiomeSource extends BiomeSource { LinkedHashSet possible = new LinkedHashSet<>(); String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT); for (IrisBiome irisBiome : engine.getAllBiomes()) { - String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey()); + String derivative = normalizeKey(irisBiome.getStructureDerivativeKey()); if (derivative != null) { possible.add(derivative); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java index e6abd0bb1..ec457e522 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java @@ -28,6 +28,7 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisNativeStructureDecision; +import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisStructureStiltSettings; import art.arcane.iris.engine.object.NativeStructureGenerationStatus; import art.arcane.iris.nativegen.NativeStructureGenerationException; @@ -611,7 +612,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { } static Set collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) { - return collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()); + LinkedHashSet keys = new LinkedHashSet<>( + collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey())); + for (IrisRegion region : dimension.getAllRegions(() -> data)) { + if (region == null) { + continue; + } + if (!region.getSeaBiomes().isEmpty()) { + keys.add("minecraft:the_void"); + } + if (!region.getShoreBiomes().isEmpty()) { + keys.add("minecraft:beach"); + } + } + return Set.copyOf(keys); } static Set collectConfiguredBiomeKeys(Iterable biomes, String dimensionLoadKey) { @@ -621,7 +635,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { if (irisBiome == null) { continue; } - Identifier derivative = Identifier.tryParse(irisBiome.getVanillaDerivativeKey()); + Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey()); if (derivative != null) { keys.add(derivative.toString().toLowerCase(Locale.ROOT)); } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java index 2546a7290..010d5153e 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/IrisModdedStructureParityTest.java @@ -4,6 +4,7 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.engine.object.InferredType; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.flag.MantleFlag; import net.minecraft.SharedConstants; @@ -101,11 +102,18 @@ public class IrisModdedStructureParityTest { IrisBiome custom = new IrisBiome() .setDerivative("forest") .setCustomDerivitives(new KList<>(new IrisBiomeCustom().setId("Aurora"))); + IrisBiome shore = new IrisBiome() + .setVanillaDerivative("minecraft:desert") + .setInferredType(InferredType.SHORE); + IrisBiome unsafeSea = new IrisBiome() + .setVanillaDerivative("minecraft:plains") + .setInferredType(InferredType.SEA); Set keys = IrisModdedChunkGenerator.collectConfiguredBiomeKeys( - List.of(ocean, custom), "OverWorld"); + List.of(ocean, custom, shore, unsafeSea), "OverWorld"); - assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "overworld:aurora"), keys); + assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach", + "minecraft:the_void", "overworld:aurora"), keys); assertFalse(keys.contains("minecraft:desert")); assertFalse(keys.contains("minecraft:plains")); } diff --git a/build.gradle b/build.gradle index 50a05535c..a94a01f86 100644 --- a/build.gradle +++ b/build.gradle @@ -113,6 +113,8 @@ nmsBindings.each { key, value -> rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/IrisChunkGenerator.java").absolutePath) systemProperty('iris.nativeStructurePostProcessorSource', rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath) + systemProperty('iris.customBiomeSource', + rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath) } } } diff --git a/core/src/main/java/art/arcane/iris/core/structure/NativeStructureLocateCapability.java b/core/src/main/java/art/arcane/iris/core/structure/NativeStructureLocateCapability.java deleted file mode 100644 index dffc707d0..000000000 --- a/core/src/main/java/art/arcane/iris/core/structure/NativeStructureLocateCapability.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure; - -public final class NativeStructureLocateCapability { - private static final String MONUMENT_KEY = "minecraft:monument"; - private static final String UNAVAILABLE_MESSAGE = "Native monument generation is enabled, but synchronous monument locating is unavailable because a cold search can stall the server thread. The locate request was not run."; - - private NativeStructureLocateCapability() { - } - - public static boolean isPaperUnavailable(String structureKey) { - return structureKey != null && MONUMENT_KEY.equalsIgnoreCase(structureKey.trim()); - } - - public static String unavailableMessage() { - return UNAVAILABLE_MESSAGE; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java index d19113151..372fb8e0c 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -41,6 +41,8 @@ import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.IrisStructureStiltSettings; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; @@ -67,7 +69,6 @@ public class IrisStructureComponent extends IrisMantleComponent { private static final double OVERBORE_MIN_BOUNDARY_SQUARED = OVERBORE_MIN_BOUNDARY * OVERBORE_MIN_BOUNDARY; private static final double OVERBORE_BOUNDARY_SPAN = 0.8; private static final double OVERBORE_MAX_UP_REACH = 1.8; - private static final int DYNAMIC_STRUCTURE_Y_TOLERANCE = 32; private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3); public IrisStructureComponent(EngineMantle engineMantle) { @@ -409,18 +410,19 @@ public class IrisStructureComponent extends IrisMantleComponent { private void clearIntersectingObjectTrees(MantleWriter writer, IrisStructureLocator.ResolvedPlacement resolved) { int mantleOffset = getEngineMantle().getEngine().getMinHeight(); int worldHeight = getEngineMantle().getEngine().getHeight(); - int verticalTolerance = resolved.exactY() ? 0 : DYNAMIC_STRUCTURE_Y_TOLERANCE; Map intersectingObjects = new HashMap<>(); for (PlacedStructurePiece piece : resolved.pieces()) { - int minY = Math.max(0, piece.getMinY() - mantleOffset - verticalTolerance); - int maxY = Math.min(worldHeight - 1, piece.getMaxY() - mantleOffset + verticalTolerance); - if (minY > maxY) { - continue; - } - for (int x = piece.getMinX(); x <= piece.getMaxX(); x++) { - for (int z = piece.getMinZ(); z <= piece.getMaxZ(); z++) { - collectIntersectingObjectTreeMarkers( - writer, x, z, minY, maxY, intersectingObjects); + for (IrisBlockVector local : piece.getObject().getBlocks().keys()) { + PlatformBlockState structureState = piece.getObject().getBlocks().get(local); + if (!isOccupiedStructureState(structureState)) { + continue; + } + IrisBlockVector rotated = piece.getRotation().rotate(local.clone()); + int y = piece.getY() - mantleOffset + rotated.getBlockY(); + if (y >= 0 && y < worldHeight) { + collectIntersectingObjectTreeMarker( + writer, piece.getX() + rotated.getBlockX(), y, + piece.getZ() + rotated.getBlockZ(), intersectingObjects); } } } @@ -429,19 +431,20 @@ public class IrisStructureComponent extends IrisMantleComponent { } } - private void collectIntersectingObjectTreeMarkers(MantleWriter writer, int x, int z, - int minY, int maxY, - Map intersectingObjects) { - for (int y = minY; y <= maxY; y++) { - PlatformBlockState state = writer.getDataIfPresent(x, y, z, PlatformBlockState.class); - if (state == null || !state.isTreeBlock()) { - continue; - } - String marker = writer.getDataIfPresent(x, y, z, String.class); - if (isOrdinaryObjectMarker(marker)) { - intersectingObjects.computeIfAbsent(marker, ignored -> new ObjectMarkerBounds()) - .include(x, y, z); - } + static boolean isOccupiedStructureState(PlatformBlockState state) { + return !B.isAir(state); + } + + private void collectIntersectingObjectTreeMarker(MantleWriter writer, int x, int y, int z, + Map intersectingObjects) { + PlatformBlockState state = writer.getDataIfPresent(x, y, z, PlatformBlockState.class); + if (state == null || !state.isTreeBlock()) { + return; + } + String marker = writer.getDataIfPresent(x, y, z, String.class); + if (isOrdinaryObjectMarker(marker)) { + intersectingObjects.computeIfAbsent(marker, ignored -> new ObjectMarkerBounds()) + .include(x, y, z); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 0db91b014..53afec4ae 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -144,7 +144,7 @@ public class IrisBiome extends IrisRegistrant implements IRare { @Desc("The raw derivative of this biome. This is required or the terrain will not properly generate. Use any vanilla biome type. Look in examples/biome-list.txt") private String derivative = "minecraft:the_void"; @Required - @Desc("Override the derivative when vanilla places structures to this derivative. This is useful for example if you have an ocean biome, but you have set the derivative to desert to get a brown-ish color. To prevent desert structures from spawning on top of your ocean, you can set your vanillaDerivative to ocean, to allow for vanilla structures. Not defining this value will simply select the derivative.") + @Desc("Override the derivative used for vanilla structure selection. Iris still enforces the generated terrain role: land-only Minecraft derivatives on sea biomes expose no native structure biome, and land-only derivatives on shore biomes resolve as beach, while exact ocean, river, beach, and shore variants remain eligible. Non-Minecraft namespaces remain authoritative. Not defining this value selects derivative.") private String vanillaDerivative = null; @ArrayType(min = 1, type = String.class) @Desc("You can instead specify multiple biome derivatives to randomly scatter colors in this biome") @@ -315,6 +315,20 @@ public class IrisBiome extends IrisRegistrant implements IRare { return resolved == null ? namespacedBiomeKey(derivative) : resolved; } + public String getStructureDerivativeKey() { + String key = getVanillaDerivativeKey(); + if (key == null || !key.startsWith("minecraft:")) { + return key; + } + if (isSea() && !isVanillaSeaStructureBiome(key)) { + return "minecraft:the_void"; + } + if (isShore() && !isVanillaShoreStructureBiome(key)) { + return "minecraft:beach"; + } + return key; + } + public String getDerivativeKey() { return namespacedBiomeKey(derivative); } @@ -327,6 +341,14 @@ public class IrisBiome extends IrisRegistrant implements IRare { return trimmed.indexOf(':') >= 0 ? trimmed : "minecraft:" + trimmed; } + static boolean isVanillaSeaStructureBiome(String key) { + return key != null && (key.contains("ocean") || key.endsWith("river")); + } + + static boolean isVanillaShoreStructureBiome(String key) { + return key != null && (key.endsWith("beach") || key.endsWith("shore")); + } + private KList getBiomeScatterResolved() { return biomeScatterResolved.aquire(() -> resolveBiomeKeys(biomeScatter)); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java index 0593e042e..b9b64ec4b 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java @@ -52,7 +52,7 @@ public class IrisImportedStructureControl { private boolean datapackOverrides = true; @ArrayType(type = IrisVanillaStructureAdjustment.class, min = 1) - @Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. Surface-intersecting structures clear logs and leaves automatically; a matching vegetation option can force clearing for unusual placements. The last matching entry with stilt settings controls foundation columns. A structure suppressed by an Iris placement is unaffected.") + @Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. A matching vegetation option explicitly clears logs and leaves inside structure piece bounds. The last matching entry with stilt settings controls foundation columns. A structure suppressed by an Iris placement is unaffected.") private KList adjustments = new KList<>(); public boolean shouldGenerate(String key) { diff --git a/core/src/test/java/art/arcane/iris/core/structure/NativeStructureLocateCapabilityTest.java b/core/src/test/java/art/arcane/iris/core/structure/NativeStructureLocateCapabilityTest.java deleted file mode 100644 index a6485304d..000000000 --- a/core/src/test/java/art/arcane/iris/core/structure/NativeStructureLocateCapabilityTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Servers - * Copyright (c) 2026 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.core.structure; - -import art.arcane.iris.engine.object.IrisImportedStructureControl; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class NativeStructureLocateCapabilityTest { - @Test - public void monumentLocateIsUnavailableWithoutChangingGenerationPolicy() { - assertTrue(NativeStructureLocateCapability.isPaperUnavailable("minecraft:monument")); - assertTrue(NativeStructureLocateCapability.isPaperUnavailable(" MINECRAFT:MONUMENT ")); - assertTrue(new IrisImportedStructureControl().shouldGenerate("minecraft:monument")); - } - - @Test - public void otherStructuresRemainLocatable() { - assertFalse(NativeStructureLocateCapability.isPaperUnavailable(null)); - assertFalse(NativeStructureLocateCapability.isPaperUnavailable("")); - assertFalse(NativeStructureLocateCapability.isPaperUnavailable("minecraft:stronghold")); - } - - @Test - public void messageStatesThatGenerationRemainsEnabled() { - String message = NativeStructureLocateCapability.unavailableMessage().toLowerCase(); - assertTrue(message.contains("generation is enabled")); - assertTrue(message.contains("locat")); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentMarkerTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentMarkerTest.java index 3c2b1b177..081d58f29 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentMarkerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisStructureComponentMarkerTest.java @@ -41,6 +41,18 @@ public class IrisStructureComponentMarkerTest { assertNotEquals(first, moved); } + @Test + public void treeCollisionUsesOnlyNonAirStructureBlocks() { + PlatformBlockState solid = mock(PlatformBlockState.class); + PlatformBlockState air = mock(PlatformBlockState.class); + when(solid.isAir()).thenReturn(false); + when(air.isAir()).thenReturn(true); + + assertTrue(IrisStructureComponent.isOccupiedStructureState(solid)); + assertFalse(IrisStructureComponent.isOccupiedStructureState(air)); + assertFalse(IrisStructureComponent.isOccupiedStructureState(null)); + } + @Test public void emptyResolvedPiecesRemainSkippableForNonReplacement() { IrisStructureLocator.ResolvedPlacement resolved = resolvedPlacement( diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeStructureDerivativeTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeStructureDerivativeTest.java new file mode 100644 index 000000000..9fd2f6ba7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisBiomeStructureDerivativeTest.java @@ -0,0 +1,67 @@ +package art.arcane.iris.engine.object; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class IrisBiomeStructureDerivativeTest { + @Test + public void seaBiomeRejectsLandOnlyVanillaDerivativeForStructures() { + IrisBiome biome = new IrisBiome() + .setDerivative("minecraft:warm_ocean") + .setVanillaDerivative("minecraft:desert") + .setInferredType(InferredType.SEA); + + assertEquals("minecraft:desert", biome.getVanillaDerivativeKey()); + assertEquals("minecraft:the_void", biome.getStructureDerivativeKey()); + } + + @Test + public void shoreBiomeRejectsLandOnlyVanillaDerivativeForStructures() { + IrisBiome biome = new IrisBiome() + .setDerivative("minecraft:warm_ocean") + .setVanillaDerivative("minecraft:savanna") + .setInferredType(InferredType.SHORE); + + assertEquals("minecraft:beach", biome.getStructureDerivativeKey()); + } + + @Test + public void waterStructureDerivativesKeepTheirExactVariant() { + IrisBiome deepOcean = new IrisBiome() + .setVanillaDerivative("minecraft:deep_cold_ocean") + .setInferredType(InferredType.SEA); + IrisBiome snowyBeach = new IrisBiome() + .setVanillaDerivative("minecraft:snowy_beach") + .setInferredType(InferredType.SHORE); + + assertEquals("minecraft:deep_cold_ocean", deepOcean.getStructureDerivativeKey()); + assertEquals("minecraft:snowy_beach", snowyBeach.getStructureDerivativeKey()); + } + + @Test + public void seaAndShoreDoNotBorrowEachOthersStructureBiomes() { + IrisBiome seaBeach = new IrisBiome() + .setVanillaDerivative("minecraft:beach") + .setInferredType(InferredType.SEA); + IrisBiome shoreOcean = new IrisBiome() + .setVanillaDerivative("minecraft:ocean") + .setInferredType(InferredType.SHORE); + + assertEquals("minecraft:the_void", seaBeach.getStructureDerivativeKey()); + assertEquals("minecraft:beach", shoreOcean.getStructureDerivativeKey()); + } + + @Test + public void landAndModdedDerivativesRemainAuthoritative() { + IrisBiome land = new IrisBiome() + .setVanillaDerivative("minecraft:desert") + .setInferredType(InferredType.LAND); + IrisBiome moddedSea = new IrisBiome() + .setVanillaDerivative("example:abyss") + .setInferredType(InferredType.SEA); + + assertEquals("minecraft:desert", land.getStructureDerivativeKey()); + assertEquals("example:abyss", moddedSea.getStructureDerivativeKey()); + } +}