Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.BrushableBlock;
import org.bukkit.block.Chest;
import org.bukkit.block.data.Brushable;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
Expand Down Expand Up @@ -140,7 +142,24 @@ private record BrushSession(BukkitTask task, Block block) {}
}

private static final Random RAND = new Random();


/**
* Multiface plants, mapped to the block they are grown on when the magic block has nothing
* for them to attach to. Placed with their default block data these plants have no face set,
* a state that vanilla deletes at the next block update and that bone meal cannot spread from.
*/
private static final Map<Material, Material> MULTIFACE_SUPPORT = Map.of(
Material.GLOW_LICHEN, Material.MOSS_BLOCK,
Material.SCULK_VEIN, Material.SCULK,
Material.RESIN_CLUMP, Material.STONE,
Material.VINE, Material.MOSS_BLOCK);

/**
* Directions tried, in order, when a multiface plant has to be given a block to grow on.
*/
private static final List<BlockFace> MULTIFACE_OFFSETS = List.of(BlockFace.UP, BlockFace.NORTH,
BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST, BlockFace.DOWN);

/**
* Constructs the BlockListener.
* @param addon - The ChunkBlock addon instance.
Expand Down Expand Up @@ -710,6 +729,10 @@ private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block)
return;
}
Material type = nextBlock.getMaterial();
if (MULTIFACE_SUPPORT.containsKey(type)) {
spawnMultifaceBlock(block, type);
return;
}
block.setType(type, false);
if (type.equals(Material.CHEST) && nextBlock.getChest() != null) {
fillChest(nextBlock, block);
Expand All @@ -720,6 +743,63 @@ private void spawnBlock(@NonNull OneBlockObject nextBlock, @NonNull Block block)
}
}

/**
* Spawns a multiface plant - glow lichen, sculk vein, resin clump or vines.
* <p>
* These blocks are a film on the face of a neighboring block, not a cube. Placing one with
* {@code setType} gives it its default block data, which has no face set at all. The client
* draws that state on all six sides, so it looks fine, but the server treats it as
* unsupported: the next block update deletes it, and bone meal has no face to spread from.
* <p>
* So attach it to whatever solid neighbors the magic block already has. If it is floating in
* mid-air there is nothing to cling to - and nothing for bone meal to spread onto either - so
* the magic block becomes the plant's support block and the plant grows on the first free
* side of it.
*
* @param block The magic block being replaced.
* @param type The multiface material, e.g. {@link Material#GLOW_LICHEN}.
*/
private void spawnMultifaceBlock(@NonNull Block block, @NonNull Material type) {
if (!(type.createBlockData() instanceof MultipleFacing plant)) {
// Not a multiface block on this server version, so place it as-is
block.setType(type, false);
return;
}
boolean attached = false;
for (BlockFace face : plant.getAllowedFaces()) {
if (canAttachTo(block.getRelative(face), face.getOppositeFace())) {
plant.setFace(face, true);
attached = true;
}
}
if (attached) {
block.setBlockData(plant, false);
return;
}
// Nothing to grow on, so grow the plant a block to live on
block.setType(MULTIFACE_SUPPORT.get(type), false);
for (BlockFace offset : MULTIFACE_OFFSETS) {
BlockFace face = offset.getOppositeFace();
Block target = block.getRelative(offset);
if (plant.getAllowedFaces().contains(face) && target.getType().isAir()) {
plant.setFace(face, true);
target.setBlockData(plant, false);
return;
}
}
}

/**
* Checks whether a multiface plant can attach itself to the given face of a block.
*
* @param block The neighboring block.
* @param face The face of that block the plant would sit on.
* @return {@code true} if that face is a full, solid face.
*/
private boolean canAttachTo(@NonNull Block block, @NonNull BlockFace face) {
return !block.getType().isAir() && block.getBlockData().isFaceSturdy(face, BlockSupport.FULL);
}

/**
* Sets a leaves block to persistent so it does not decay.
* @param block The leaves block.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

Expand All @@ -38,11 +40,15 @@
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.BrushableBlock;
import org.bukkit.block.Chest;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Brushable;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Item;
import org.bukkit.event.EventHandler;
Expand All @@ -62,6 +68,8 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockbukkit.mockbukkit.block.data.MultipleFacingDataMock;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
Expand Down Expand Up @@ -601,6 +609,105 @@ void testSpawnBlockRegularMaterial() throws Exception {
verify(magicBlock).setType(Material.STONE, false);
}

/**
* Stubs every neighbor of the magic block as air.
* @return map of the neighbor mocks, keyed by the face of the magic block they sit on.
*/
private Map<BlockFace, Block> stubAirNeighbors(Material plant) {
// Bukkit is statically mocked here, so supply the multiface block data ourselves.
// MockBukkit does not model every multiface material, so build the mock directly.
mockedBukkit.when(() -> Bukkit.createBlockData(plant))
.thenAnswer(i -> new MultipleFacingDataMock(plant));
return stubAirNeighbors();
}

private Map<BlockFace, Block> stubAirNeighbors() {
Map<BlockFace, Block> neighbors = new EnumMap<>(BlockFace.class);
for (BlockFace face : List.of(BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.EAST,
BlockFace.SOUTH, BlockFace.WEST)) {
Block neighbor = mock(Block.class);
when(neighbor.getType()).thenReturn(Material.AIR);
when(magicBlock.getRelative(face)).thenReturn(neighbor);
neighbors.put(face, neighbor);
}
return neighbors;
}

/**
* Turns a neighbor mock into a solid block that a multiface plant can attach to.
* @param neighbor the neighbor mock
* @param face the face of the neighbor that the plant would sit on
*/
private void makeSolid(Block neighbor, BlockFace face) {
BlockData data = mock(BlockData.class);
when(neighbor.getType()).thenReturn(Material.MOSS_BLOCK);
when(neighbor.getBlockData()).thenReturn(data);
when(data.isFaceSturdy(face, BlockSupport.FULL)).thenReturn(true);
}

/**
* Test method for
* {@link world.bentobox.chunkblock.listeners.BlockListener} spawnBlock — GLOW_LICHEN with support.
* Glow lichen is attached to the faces of the solid neighbors the magic block has, so that
* vanilla does not delete it and bone meal has a face to spread from.
*/
@Test
void testSpawnBlockGlowLichenAttachesToNeighbors() throws Exception {
Map<BlockFace, Block> neighbors = stubAirNeighbors(Material.GLOW_LICHEN);
makeSolid(neighbors.get(BlockFace.DOWN), BlockFace.UP);
makeSolid(neighbors.get(BlockFace.NORTH), BlockFace.SOUTH);

callSpawnBlock(new OneBlockObject(Material.GLOW_LICHEN, 1), magicBlock);

ArgumentCaptor<BlockData> captor = ArgumentCaptor.forClass(BlockData.class);
verify(magicBlock).setBlockData(captor.capture(), eq(false));
MultipleFacing lichen = (MultipleFacing) captor.getValue();
assertEquals(Material.GLOW_LICHEN, lichen.getMaterial());
assertEquals(Set.of(BlockFace.DOWN, BlockFace.NORTH), lichen.getFaces());
// The magic block itself is the lichen, so no support block is placed
verify(magicBlock, never()).setType(any(Material.class), anyBoolean());
}

/**
* Test method for
* {@link world.bentobox.chunkblock.listeners.BlockListener} spawnBlock — GLOW_LICHEN in mid-air.
* With nothing to cling to, the magic block becomes moss and the lichen grows on top of it.
*/
@Test
void testSpawnBlockGlowLichenWithNoSupport() throws Exception {
Map<BlockFace, Block> neighbors = stubAirNeighbors(Material.GLOW_LICHEN);

callSpawnBlock(new OneBlockObject(Material.GLOW_LICHEN, 1), magicBlock);

verify(magicBlock).setType(Material.MOSS_BLOCK, false);
ArgumentCaptor<BlockData> captor = ArgumentCaptor.forClass(BlockData.class);
verify(neighbors.get(BlockFace.UP)).setBlockData(captor.capture(), eq(false));
MultipleFacing lichen = (MultipleFacing) captor.getValue();
assertEquals(Material.GLOW_LICHEN, lichen.getMaterial());
assertEquals(Set.of(BlockFace.DOWN), lichen.getFaces());
}

/**
* Test method for
* {@link world.bentobox.chunkblock.listeners.BlockListener} spawnBlock — VINE in mid-air.
* Vines cannot hang off the underside of a block, so they are grown on the side of the
* support block instead.
*/
@Test
void testSpawnBlockVineWithNoSupport() throws Exception {
Map<BlockFace, Block> neighbors = stubAirNeighbors(Material.VINE);

callSpawnBlock(new OneBlockObject(Material.VINE, 1), magicBlock);

verify(magicBlock).setType(Material.MOSS_BLOCK, false);
verify(neighbors.get(BlockFace.UP), never()).setBlockData(any(), anyBoolean());
ArgumentCaptor<BlockData> captor = ArgumentCaptor.forClass(BlockData.class);
verify(neighbors.get(BlockFace.NORTH)).setBlockData(captor.capture(), eq(false));
MultipleFacing vine = (MultipleFacing) captor.getValue();
assertEquals(Material.VINE, vine.getMaterial());
assertEquals(Set.of(BlockFace.SOUTH), vine.getFaces());
}

/**
* Test method for
* {@link world.bentobox.chunkblock.listeners.BlockListener} spawnBlock — CHEST with items.
Expand Down
Loading