Merge remote-tracking branch 'origin/master'

This commit is contained in:
Robert Stoneking committed 2026-09-10 01:17:34 -06:00
commit 5ba95388f8
10 files changed
+444 -35

No files matched your search

@@ -74,6 +74,10 @@ public class IrisEntitySpawn implements IRare {
private transient IrisMarker referenceMarker;
public int spawn(Engine gen, Chunk c, RNG rng) {
IrisEntity definition = getRealEntity(gen);
if (definition == null) {
return 0;
}
int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns);
int s = 0;
@@ -84,11 +88,10 @@ public class IrisEntitySpawn implements IRare {
World world = c.getWorld();
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
Location l = switch (getReferenceSpawner().getGroup()) {
case NORMAL -> new Location(c.getWorld(), x, hf + 1, z);
case CAVE -> findCaveSpawnLocation(gen, c, rng);
case UNDERWATER, BEACH -> new Location(c.getWorld(), x, rng.i(h + 1, hf), z);
};
IrisSpawnGroup group = getReferenceSpawner().getGroup();
Integer y = selectSurfaceSpawnY(group, definition.getSurface(), h, hf, rng);
Location l = group == IrisSpawnGroup.CAVE ? findCaveSpawnLocation(gen, c, rng)
: y == null ? null : new Location(world, x, y, z);
if (l != null) {
if (referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) {
@@ -109,6 +112,17 @@ public class IrisEntitySpawn implements IRare {
return s;
}
public static Integer selectSurfaceSpawnY(IrisSpawnGroup group, IrisSurface surface,
int floor, int top, RNG rng) {
if (group == IrisSpawnGroup.CAVE) {
return null;
}
if (group == IrisSpawnGroup.NORMAL && !surface.isFluid()) {
return top + 1;
}
return top > floor ? LootResolver.inclusive(rng, floor + 1, top) : null;
}
public int spawn(Engine gen, IrisPosition c, RNG rng) {
int spawns = LootResolver.inclusive(rng, minSpawns, maxSpawns);
int s = 0;
@@ -198,19 +212,21 @@ public class IrisEntitySpawn implements IRare {
return null;
}
if (!ignoreSurfaces && !irisEntity.getSurface().matches(at.clone().subtract(0, 1, 0).getBlock())) {
IrisSurface surface = irisEntity.getSurface();
boolean checkPosition = !ignoreSurfaces || surface.isFluid();
if (checkPosition && !surface.matches(at.clone().subtract(0, surface.isFluid() ? 0 : 1, 0).getBlock())) {
return null;
}
Vector3d boundingBox = BukkitPlatform.entityBoundingBox(irisEntity.getBukkitType());
if (!ignoreSurfaces && boundingBox != null) {
boolean isClearForSpawn = isAreaClearForSpawn(at, boundingBox);
if (checkPosition && boundingBox != null) {
boolean isClearForSpawn = isAreaClearForSpawn(at, boundingBox, surface);
if (!isClearForSpawn) {
return null;
}
}
Entity e = irisEntity.spawn(g, at.add(0.5, 0.5, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity())));
Entity e = irisEntity.spawn(g, at.clone().add(0.5, 0.5, 0.5), rng.aquire(() -> new RNG(g.getSeedManager().getEntity())));
if (e != null) {
IrisLogging.debug("Spawned " + C.DARK_AQUA + "Entity<" + getEntity() + "> " + C.GREEN + e.getType() + C.LIGHT_PURPLE + " @ " + C.GRAY + e.getLocation().getX() + ", " + e.getLocation().getY() + ", " + e.getLocation().getZ());
}
@@ -223,19 +239,26 @@ public class IrisEntitySpawn implements IRare {
}
}
private boolean isAreaClearForSpawn(Location center, Vector3d boundingBox) {
private boolean isAreaClearForSpawn(Location center, Vector3d boundingBox, IrisSurface surface) {
World world = center.getWorld();
int startX = center.getBlockX() - (int) (boundingBox.x / 2);
int endX = center.getBlockX() + (int) (boundingBox.x / 2);
int startY = center.getBlockY();
int endY = center.getBlockY() + (int) boundingBox.y;
int startZ = center.getBlockZ() - (int) (boundingBox.z / 2);
int endZ = center.getBlockZ() + (int) (boundingBox.z / 2);
boolean fluid = surface.isFluid();
int startX = fluid ? (int) Math.floor(center.getX() + 0.5 - boundingBox.x / 2)
: center.getBlockX() - (int) (boundingBox.x / 2);
int endX = fluid ? (int) Math.floor(Math.nextDown(center.getX() + 0.5 + boundingBox.x / 2))
: center.getBlockX() + (int) (boundingBox.x / 2);
int startY = fluid ? (int) Math.floor(center.getY() + 0.5) : center.getBlockY();
int endY = fluid ? (int) Math.floor(Math.nextDown(center.getY() + 0.5 + boundingBox.y))
: center.getBlockY() + (int) boundingBox.y;
int startZ = fluid ? (int) Math.floor(center.getZ() + 0.5 - boundingBox.z / 2)
: center.getBlockZ() - (int) (boundingBox.z / 2);
int endZ = fluid ? (int) Math.floor(Math.nextDown(center.getZ() + 0.5 + boundingBox.z / 2))
: center.getBlockZ() + (int) (boundingBox.z / 2);
for (int x = startX; x <= endX; x++) {
for (int y = startY; y <= endY; y++) {
for (int z = startZ; z <= endZ; z++) {
if (world.getBlockAt(x, y, z).getType() != Material.AIR) {
if (fluid ? !surface.matches(world.getBlockAt(x, y, z))
: world.getBlockAt(x, y, z).getType() != Material.AIR) {
return false;
}
}
@@ -22,7 +22,7 @@ public class IrisStructureTerrain {
private static final double MAX_EROSION_FREQUENCY = 1D;
private static final double MAX_LOBE_FREQUENCY = 1D;
@Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. FLATTEN cuts and fills exposed native foundations within flattenRange, blending across horizontalPadding blocks. VACUUM raises terrain from processed rigid-template foundations with a fixed 12-block falloff without lowering ground. BORE and FORCE_CARVE clear the requested envelope, while ENCASE fills it before placement.")
@Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. FLATTEN cuts and fills exposed native structure terrain within flattenRange, blending across horizontalPadding blocks. VACUUM raises terrain from processed rigid-template foundations with a fixed 12-block falloff without lowering ground. BORE and FORCE_CARVE clear the requested envelope, while ENCASE fills it before placement.")
private IrisStructureTerrainMode mode = IrisStructureTerrainMode.SOURCE;
@MinNumber(0)
@@ -41,6 +41,10 @@ public enum IrisSurface {
@Desc("Within lava")
LAVA;
public boolean isFluid() {
return this == WATER || this == LAVA;
}
/**
* Check if this Iris surface matches the blockstate provided
*
@@ -49,7 +53,7 @@ public enum IrisSurface {
*/
public boolean matches(Block state) {
Material type = state.getType();
if (type.isSolid()) {
if (state.isSolid()) {
return this == LAND || this == OVERWORLD || (this == ANIMAL
&& (type == Material.GRASS_BLOCK || type == Material.DIRT
|| type == Material.DIRT_PATH || type == Material.COARSE_DIRT
@@ -60,7 +64,7 @@ public enum IrisSurface {
if (type == Material.WATER || type == Material.SEAGRASS
|| type == Material.TALL_SEAGRASS || type == Material.KELP_PLANT
|| type == Material.KELP ||
(state instanceof Waterlogged && ((Waterlogged) state).isWaterlogged()))
(state.getBlockData() instanceof Waterlogged waterlogged && waterlogged.isWaterlogged()))
return this == WATER || this == OVERWORLD;
return false;
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "b205ce33025fa36115f9ddb38c704803875043950967e31449e2b08ae364a7fe";
private static final String EXPECTED_SCHEMA_DIGEST = "43b3add0a559f7006e2973344f39db933bbfb1be889f3c5d616a7bf035a9db39";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -1,14 +1,221 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.Vector3d;
import org.bukkit.Chunk;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
public class IrisEntitySpawnTest {
@Test
public void caveSpawnWithoutSafeMarkerIsSkipped() {
assertNull(IrisEntitySpawn.selectCaveSpawnLocation(new KList<>(), null, new RNG(1L)));
}
@Test
public void mixedNormalSpawnerPlacesEveryTadpoleInsideOneBlockDeepWater() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 77, 78))) {
assertEquals(3, fixture.spawn(IrisSpawnGroup.NORMAL));
assertEquals(3, fixture.positions.size());
assertTrue(fixture.positions.stream().allMatch(position -> position.getY() == 78.5));
}
}
@Test
public void underwaterSpawnerAcceptsWaterInsteadOfRequiringAir() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 65, 78))) {
assertEquals(3, fixture.spawn(IrisSpawnGroup.UNDERWATER));
assertTrue(fixture.positions.stream().allMatch(position -> position.getBlockY() > 65
&& position.getBlockY() <= 78));
}
}
@Test
public void waterEntityCannotSpawnInAirAboveWater() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 77, 78))) {
assertNull(fixture.entry.spawn(fixture.engine, new Location(fixture.world, 0, 79, 0)));
assertTrue(fixture.positions.isEmpty());
}
}
@Test
public void dryColumnsDoNotSpawnWaterEntities() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 78, 78))) {
assertEquals(0, fixture.spawn(IrisSpawnGroup.NORMAL));
assertEquals(0, fixture.spawn(IrisSpawnGroup.UNDERWATER));
}
}
@Test
public void mixedNormalSpawnerRetainsLandEntityPlacement() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78))) {
assertEquals(3, fixture.spawn(IrisSpawnGroup.NORMAL));
assertTrue(fixture.positions.stream().allMatch(position -> position.getY() == 79.5));
}
}
@Test
public void lavaEntityOccupiesLavaAndRejectsWater() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAVA, Material.LAVA, 77, 78))) {
assertEquals(3, fixture.spawn(IrisSpawnGroup.NORMAL));
}
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAVA, Material.WATER, 77, 78))) {
assertEquals(0, fixture.spawn(IrisSpawnGroup.NORMAL));
}
}
@Test
public void fluidVolumeIncludesTheCenteredEntityHeight() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 77, 78))) {
fixture.platform.when(() -> BukkitPlatform.entityBoundingBox(EntityType.TADPOLE))
.thenReturn(new Vector3d(0.4, 0.8, 0.4));
assertNull(fixture.entry.spawn(fixture.engine, new Location(fixture.world, 0, 78, 0)));
fixture.platform.when(() -> BukkitPlatform.entityBoundingBox(EntityType.TADPOLE))
.thenReturn(new Vector3d(0.4, 0.5, 0.4));
fixture.entry.spawn(fixture.engine, new Location(fixture.world, 0, 78, 0));
assertEquals(1, fixture.positions.size());
}
}
@Test
public void fluidVolumeRejectsWaterloggedSolidObstructionBesideWideEntity() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 77, 79))) {
fixture.platform.when(() -> BukkitPlatform.entityBoundingBox(EntityType.TADPOLE))
.thenReturn(new Vector3d(1.3, 0.8, 1.3));
Block obstruction = mock(Block.class);
Waterlogged data = mock(Waterlogged.class);
when(data.isWaterlogged()).thenReturn(true);
when(obstruction.getBlockData()).thenReturn(data);
when(obstruction.getType()).thenReturn(Material.OAK_FENCE);
when(obstruction.isSolid()).thenReturn(true);
when(fixture.world.getBlockAt(-2, 78, -1)).thenReturn(obstruction);
assertNull(fixture.entry.spawn(fixture.engine, new Location(fixture.world, -1, 78, -1)));
}
}
@Test
public void waterloggedNonSolidPlantsCountAsWater() {
Block block = mock(Block.class);
Waterlogged data = mock(Waterlogged.class);
when(block.getType()).thenReturn(Material.SEA_PICKLE);
when(block.getBlockData()).thenReturn(data);
when(data.isWaterlogged()).thenReturn(true);
assertTrue(IrisSurface.WATER.matches(block));
}
@Test
public void markerWaterSpawnsStillRequireSubmergedBodies() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.WATER, Material.WATER, 77, 78));
MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class)) {
binding.when(() -> BukkitWorldBinding.tryBind(null)).thenReturn(true);
binding.when(() -> BukkitWorldBinding.world(null)).thenReturn(fixture.world);
fixture.platform.when(() -> BukkitPlatform.toLocation(any(IrisPosition.class), eq(fixture.world)))
.thenAnswer(invocation -> {
IrisPosition position = invocation.getArgument(0);
return new Location(fixture.world, position.getX(), position.getY(), position.getZ());
});
fixture.entry.setReferenceSpawner(new IrisSpawner());
assertEquals(0, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 78, 0), new RNG(1L)));
assertEquals(3, fixture.entry.spawn(fixture.engine, new IrisPosition(0, 77, 0), new RNG(1L)));
}
}
@Test
public void repeatedExplicitSpawnsDoNotMoveTheCallerLocation() {
try (SpawnFixture fixture = new SpawnFixture(new SpawnOptions(IrisSurface.LAND, Material.WATER, 78, 78))) {
Location location = new Location(fixture.world, 0, 79, 0);
fixture.entry.spawn(fixture.engine, location);
fixture.entry.spawn(fixture.engine, location);
assertEquals(new Location(fixture.world, 0, 79, 0), location);
assertEquals(List.of(new Location(fixture.world, 0.5, 79.5, 0.5),
new Location(fixture.world, 0.5, 79.5, 0.5)), fixture.positions);
}
}
private record SpawnOptions(IrisSurface surface, Material fluid, int floor, int top) {
}
private static final class SpawnFixture implements AutoCloseable {
private final Engine engine = mock(Engine.class);
private final World world = mock(World.class);
private final Chunk chunk = mock(Chunk.class);
private final IrisEntitySpawn entry = spy(new IrisEntitySpawn());
private final List<Location> positions = new ArrayList<>();
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
private SpawnFixture(SpawnOptions options) {
IrisSurface surface = options.surface();
Material fluid = options.fluid();
int floor = options.floor();
int top = options.top();
IrisEntity definition = mock(IrisEntity.class);
when(definition.getSurface()).thenReturn(surface);
when(definition.getBukkitType()).thenReturn(EntityType.TADPOLE);
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
when(chunk.getWorld()).thenReturn(world);
when(world.getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.OCEAN_FLOOR))).thenReturn(floor);
when(world.getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.WORLD_SURFACE))).thenReturn(top);
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int y = invocation.getArgument(1);
Block block = mock(Block.class);
when(block.getType()).thenReturn(y <= floor ? Material.STONE : y <= top ? fluid : Material.AIR);
when(block.isSolid()).thenReturn(y <= floor);
return block;
});
when(world.getBlockAt(any(Location.class))).thenAnswer(invocation -> {
Location location = invocation.getArgument(0);
return world.getBlockAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
});
when(definition.spawn(eq(engine), any(Location.class), any(RNG.class))).thenAnswer(invocation -> {
Location location = invocation.getArgument(1);
positions.add(location.clone());
Entity spawned = mock(Entity.class);
when(spawned.getLocation()).thenReturn(location.clone());
when(spawned.getType()).thenReturn(EntityType.TADPOLE);
return spawned;
});
platform.when(() -> BukkitPlatform.entityBoundingBox(EntityType.TADPOLE))
.thenReturn(new Vector3d(0.4, 0.3, 0.4));
doReturn(definition).when(entry).getRealEntity(engine);
entry.setMinSpawns(3).setMaxSpawns(3);
}
private int spawn(IrisSpawnGroup group) {
entry.setReferenceSpawner(new IrisSpawner().setGroup(group));
return entry.spawn(engine, chunk, new RNG(1337L));
}
@Override
public void close() {
platform.close();
}
}
}