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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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