mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
+48
-2
@@ -450,7 +450,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenSupportsProjectedRoadsAcrossRoofCavitiesWithoutFillingAdjacentColumns() throws Exception {
|
||||
public void flattenSupportsProjectedRoadsAndTheirSurroundingGradeAcrossRoofCavities() throws Exception {
|
||||
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(0, 256), containerFactory(), null);
|
||||
chunk.setPersistedStatus(ChunkStatus.CARVERS);
|
||||
@@ -504,11 +504,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
}
|
||||
assertTrue(chunk.getBlockState(new BlockPos(4, roadY - 33, 0)).isAir());
|
||||
int adjacentY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 5, 0);
|
||||
assertTrue(chunk.getBlockState(new BlockPos(5, adjacentY - 1, 0)).isAir());
|
||||
for (int y = adjacentY - 32; y < adjacentY; y++) {
|
||||
assertTrue("Missing ground between road cells at " + y,
|
||||
chunk.getBlockState(new BlockPos(5, y, 0)).isSolid());
|
||||
}
|
||||
assertEquals(Blocks.WATER.defaultBlockState(), chunk.getBlockState(new BlockPos(6, 50, 0)));
|
||||
assertTrue(chunk.getBlockState(new BlockPos(6, 49, 0)).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenBuildsAContinuousVillageGradeBetweenSparseFoundations() throws Exception {
|
||||
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
|
||||
List<PoolElementStructurePiece> pieces = new ArrayList<>();
|
||||
for (int x : List.of(0, 8)) {
|
||||
for (int z : List.of(0, 8)) {
|
||||
pieces.add(rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(x, 65, z, x, 75, z), 0, Rotation.NONE));
|
||||
}
|
||||
}
|
||||
StructureStart start = rigidSurfaceStart(pieces);
|
||||
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
|
||||
target.terrain().setHorizontalPadding(24).setFlattenRange(64);
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 8);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
for (int z = 0; z <= 8; z++) {
|
||||
int originalY = (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96;
|
||||
put(blocks, x, 20, z, Blocks.STONE.defaultBlockState());
|
||||
put(blocks, x, originalY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, originalY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
}
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area, List.of(target),
|
||||
(x, z) -> (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96);
|
||||
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
for (int z = 0; z <= 8; z++) {
|
||||
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
|
||||
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
|
||||
for (int y = 20; y <= groundY; y++) {
|
||||
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
|
||||
state(blocks, x, y, z).isSolid());
|
||||
}
|
||||
for (int y = groundY + 1; y <= 96; y++) {
|
||||
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
|
||||
state(blocks, x, y, z).isAir());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() {
|
||||
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
|
||||
+4
-6
@@ -750,14 +750,14 @@ public final class NativeStructureSurfaceFitter {
|
||||
if (selected != null) {
|
||||
int targetY = originalY + Math.max(-selected.range(),
|
||||
Math.min(selected.range(), selected.source().meetY() - originalY));
|
||||
return new FlattenResolution(targetY, selected.range(), selected.source().strength() > 1);
|
||||
return new FlattenResolution(targetY, selected.range());
|
||||
}
|
||||
if (totalWeight == 0) {
|
||||
return new FlattenResolution(originalY, 0, false);
|
||||
return new FlattenResolution(originalY, 0);
|
||||
}
|
||||
int targetY = blendSurfaceTarget(originalY, weightedY / (double) totalWeight,
|
||||
maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE);
|
||||
return new FlattenResolution(targetY, range, false);
|
||||
return new FlattenResolution(targetY, range);
|
||||
}
|
||||
|
||||
private static Map<Long, Integer> fitFlattenTerrain(
|
||||
@@ -780,9 +780,7 @@ public final class NativeStructureSurfaceFitter {
|
||||
int minimumY = Math.max(area.minY(), targetY - range);
|
||||
applySurfaceColumn(world, position, x, z, originalY, targetY,
|
||||
minimumY, Math.min(area.maxY(), originalY + range));
|
||||
if (resolution.foundation() || projectedRange > 0) {
|
||||
fillFlattenFoundation(world, position, x, z, targetY, minimumY);
|
||||
}
|
||||
heights.put(column, targetY + 1);
|
||||
}
|
||||
}
|
||||
@@ -1104,7 +1102,7 @@ public final class NativeStructureSurfaceFitter {
|
||||
record FlattenAnchor(SurfaceAnchor source, int radius, int range) {
|
||||
}
|
||||
|
||||
record FlattenResolution(int targetY, int range, boolean foundation) {
|
||||
record FlattenResolution(int targetY, int range) {
|
||||
}
|
||||
|
||||
private record FlattenFootprint(List<FlattenAnchor> anchors, Map<Long, Integer> projectedSupport) {
|
||||
|
||||
@@ -273,10 +273,33 @@ public final class ModdedEntitySpawner {
|
||||
if (type == null) {
|
||||
return true;
|
||||
}
|
||||
if (irisEntity.getSurface().isFluid()) {
|
||||
return isFluidAreaClearForSpawn(blockX, blockY, blockZ, type.getWidth(), type.getHeight(),
|
||||
position -> ModdedWorldManager.matchesSurface(irisEntity.getSurface(), level.getBlockState(position)));
|
||||
}
|
||||
return isAreaClearForSpawn(blockX, blockY, blockZ, type.getWidth(), type.getHeight(),
|
||||
position -> level.getBlockState(position).is(Blocks.AIR));
|
||||
}
|
||||
|
||||
static boolean isFluidAreaClearForSpawn(int blockX, int blockY, int blockZ,
|
||||
float width, float height, Predicate<BlockPos> matchesFluid) {
|
||||
int startX = (int) Math.floor(blockX + 0.5 - width / 2D);
|
||||
int endX = (int) Math.floor(Math.nextDown(blockX + 0.5 + width / 2D));
|
||||
int endY = (int) Math.floor(Math.nextDown(blockY + 0.5 + height));
|
||||
int startZ = (int) Math.floor(blockZ + 0.5 - width / 2D);
|
||||
int endZ = (int) Math.floor(Math.nextDown(blockZ + 0.5 + width / 2D));
|
||||
for (int x = startX; x <= endX; x++) {
|
||||
for (int y = blockY; y <= endY; y++) {
|
||||
for (int z = startZ; z <= endZ; z++) {
|
||||
if (!matchesFluid.test(new BlockPos(x, y, z))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean isAreaClearForSpawn(int blockX, int blockY, int blockZ, float width, float height, Predicate<BlockPos> isAir) {
|
||||
int radius = (int) (width / 2F);
|
||||
int endY = blockY + (int) height;
|
||||
|
||||
+12
-5
@@ -604,9 +604,11 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
worldZ = (chunkZ << 4) + RNG.r.i(16);
|
||||
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, worldX, worldZ) - 1;
|
||||
int solidY = level.getHeight(Heightmap.Types.OCEAN_FLOOR, worldX, worldZ) - 1;
|
||||
worldY = group == IrisSpawnGroup.NORMAL
|
||||
? surfaceY + 1
|
||||
: RNG.r.i(solidY + 1, surfaceY);
|
||||
Integer selectedY = IrisEntitySpawn.selectSurfaceSpawnY(group, irisEntity.getSurface(), solidY, surfaceY, RNG.r);
|
||||
if (selectedY == null) {
|
||||
continue;
|
||||
}
|
||||
worldY = selectedY;
|
||||
}
|
||||
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
|
||||
continue;
|
||||
@@ -653,6 +655,11 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (irisEntity.getSurface().isFluid()
|
||||
&& (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)
|
||||
|| !ModdedEntitySpawner.isAreaClearForSpawn(level, irisEntity, worldX, worldY, worldZ))) {
|
||||
continue;
|
||||
}
|
||||
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
|
||||
spawned++;
|
||||
}
|
||||
@@ -681,11 +688,11 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
||||
}
|
||||
|
||||
private boolean surfaceMatches(IrisSurface surface, ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
BlockState below = level.getBlockState(new BlockPos(worldX, worldY - 1, worldZ));
|
||||
BlockState below = level.getBlockState(new BlockPos(worldX, worldY - (surface.isFluid() ? 0 : 1), worldZ));
|
||||
return matchesSurface(surface, below);
|
||||
}
|
||||
|
||||
private static boolean matchesSurface(IrisSurface surface, BlockState below) {
|
||||
static boolean matchesSurface(IrisSurface surface, BlockState below) {
|
||||
if (ModdedBlockResolution.isSolid(below)) {
|
||||
return surface == IrisSurface.LAND || surface == IrisSurface.OVERWORLD
|
||||
|| (surface == IrisSurface.ANIMAL && isAnimalGround(below));
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||
import art.arcane.iris.engine.object.IrisSpawnGroup;
|
||||
import art.arcane.iris.engine.object.IrisSurface;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedWaterEntitySpawnTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tadpoleVolumeRequiresWaterAndAcceptsAquaticPlants() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
IrisEntity entity = new IrisEntity().setType("tadpole").setSurface(IrisSurface.WATER);
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.WATER.defaultBlockState());
|
||||
assertTrue(ModdedEntitySpawner.isAreaClearForSpawn(level, entity, 0, 78, 0));
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.SEAGRASS.defaultBlockState());
|
||||
assertTrue(ModdedEntitySpawner.isAreaClearForSpawn(level, entity, 0, 78, 0));
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.AIR.defaultBlockState());
|
||||
assertFalse(ModdedEntitySpawner.isAreaClearForSpawn(level, entity, 0, 79, 0));
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.LAVA.defaultBlockState());
|
||||
assertFalse(ModdedEntitySpawner.isAreaClearForSpawn(level, entity, 0, 78, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lavaSurfaceRequiresLavaInsteadOfAirOrWater() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
IrisEntity entity = new IrisEntity().setType("tadpole").setSurface(IrisSurface.LAVA);
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.LAVA.defaultBlockState());
|
||||
assertTrue(ModdedEntitySpawner.isAreaClearForSpawn(level, entity, 0, 78, 0));
|
||||
when(level.getBlockState(any(BlockPos.class))).thenReturn(Blocks.WATER.defaultBlockState());
|
||||
assertFalse(ModdedEntitySpawner.isAreaClearForSpawn(level, entity, 0, 78, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waterloggedSolidBlocksStillObstructWaterSpawns() {
|
||||
assertTrue(ModdedWorldManager.matchesSurface(IrisSurface.WATER,
|
||||
Blocks.SEA_PICKLE.defaultBlockState().setValue(BlockStateProperties.WATERLOGGED, true)));
|
||||
assertFalse(ModdedWorldManager.matchesSurface(IrisSurface.WATER,
|
||||
Blocks.OAK_FENCE.defaultBlockState().setValue(BlockStateProperties.WATERLOGGED, true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalFluidVolumeIncludesHorizontalEdgesAndCenteredHeight() {
|
||||
Set<BlockPos> checked = new HashSet<>();
|
||||
assertTrue(ModdedEntitySpawner.isFluidAreaClearForSpawn(-1, 78, -1, 1.3F, 0.8F, position -> {
|
||||
checked.add(position);
|
||||
return true;
|
||||
}));
|
||||
assertEquals(18, checked.size());
|
||||
assertTrue(checked.contains(new BlockPos(-2, 79, -2)));
|
||||
assertTrue(checked.contains(new BlockPos(0, 78, 0)));
|
||||
assertFalse(ModdedEntitySpawner.isFluidAreaClearForSpawn(-1, 78, -1, 1.3F, 0.8F,
|
||||
position -> !position.equals(new BlockPos(-2, 79, -2))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactUpperBoundaryDoesNotRequireWaterOutsideTheBody() {
|
||||
Set<BlockPos> checked = new HashSet<>();
|
||||
assertTrue(ModdedEntitySpawner.isFluidAreaClearForSpawn(0, 78, 0, 1F, 0.5F, position -> {
|
||||
checked.add(position);
|
||||
return true;
|
||||
}));
|
||||
assertEquals(Set.of(new BlockPos(0, 78, 0)), checked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedSpawnerUsesFluidDepthWithoutChangingLandMembers() {
|
||||
RNG rng = new RNG(1L);
|
||||
assertEquals(Integer.valueOf(78), IrisEntitySpawn.selectSurfaceSpawnY(
|
||||
IrisSpawnGroup.NORMAL, IrisSurface.WATER, 77, 78, rng));
|
||||
assertEquals(Integer.valueOf(79), IrisEntitySpawn.selectSurfaceSpawnY(
|
||||
IrisSpawnGroup.NORMAL, IrisSurface.LAND, 77, 78, rng));
|
||||
assertNull(IrisEntitySpawn.selectSurfaceSpawnY(IrisSpawnGroup.NORMAL, IrisSurface.WATER, 78, 78, rng));
|
||||
assertNull(IrisEntitySpawn.selectSurfaceSpawnY(IrisSpawnGroup.UNDERWATER, IrisSurface.WATER, 78, 78, rng));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user