Double Crates

- FlexCrates -> Adjustable Crates
- Adjustable Crates will now connect to adjacent crates and merge inventories
This commit is contained in:
simibubi 2020-01-24 22:55:20 +01:00
parent 2903b7046c
commit b8490038fd
21 changed files with 298 additions and 40 deletions

View file

@ -23,6 +23,7 @@ public enum ScreenResources {
SCHEMATICANNON_FUEL("schematicannon.png", 0, 215, 82, 4),
FLEXCRATE("flex_crate_and_stockpile_switch.png", 125, 129),
FLEXCRATE_DOUBLE("double_flexcrate.png", 197, 129),
FLEXCRATE_LOCKED_SLOT("flex_crate_and_stockpile_switch.png", 138, 0, 18, 18),
STOCKSWITCH("flex_crate_and_stockpile_switch.png", 0, 129, 205, 93),

View file

@ -39,7 +39,7 @@ public class FilteringBehaviour extends TileEntityBehaviour {
callback = stack -> {
};
textShift = Vec3d.ZERO;
count = -1;
count = 0;
ticksUntilScrollPacket = -1;
}

View file

@ -3,7 +3,9 @@ package com.simibubi.create.foundation.behaviour.inventory;
import java.util.ArrayList;
import java.util.List;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.foundation.behaviour.base.TileEntityBehaviour;
import com.simibubi.create.modules.logistics.block.inventories.FlexcrateBlock;
import net.minecraft.block.BlockState;
import net.minecraft.block.ChestBlock;
@ -45,6 +47,11 @@ public class SynchronizedExtraction {
if (blockState.get(ChestBlock.TYPE) != ChestType.SINGLE)
inventoryPositions.add(inventoryPos.offset(ChestBlock.getDirectionToAttached(blockState)));
// Sync across flexcrates
if (AllBlocks.FLEXCRATE.typeOf(blockState))
if (blockState.get(FlexcrateBlock.DOUBLE))
inventoryPositions.add(inventoryPos.offset(blockState.get(FlexcrateBlock.FACING)));
for (BlockPos pos : inventoryPositions) {
for (Direction direction : Direction.values()) {
SingleTargetAutoExtractingBehaviour behaviour = TileEntityBehaviour.get(reader, pos.offset(direction),

View file

@ -72,7 +72,8 @@ public class CrushingWheelBlock extends RotatedPillarKineticBlock {
boolean controllerShouldExist = false;
boolean controllerShouldBeValid = false;
if (AllBlocks.CRUSHING_WHEEL.typeOf(world.getBlockState(otherWheelPos))) {
BlockState otherState = world.getBlockState(otherWheelPos);
if (AllBlocks.CRUSHING_WHEEL.typeOf(otherState)) {
controllerShouldExist = true;
KineticTileEntity te = (KineticTileEntity) world.getTileEntity(pos);
KineticTileEntity otherTe = (KineticTileEntity) world.getTileEntity(otherWheelPos);
@ -80,6 +81,8 @@ public class CrushingWheelBlock extends RotatedPillarKineticBlock {
float signum = Math.signum(te.getSpeed()) * (state.get(AXIS) == Axis.X ? -1 : 1);
controllerShouldBeValid = facing.getAxisDirection().getOffset() != signum;
}
if (otherState.get(AXIS) != state.get(AXIS))
controllerShouldExist = false;
}
if (!controllerShouldExist) {

View file

@ -1,5 +1,7 @@
package com.simibubi.create.modules.logistics.block.inventories;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.foundation.block.ProperDirectionalBlock;
import com.simibubi.create.foundation.utility.AllShapes;
import net.minecraft.block.Block;
@ -7,21 +9,30 @@ import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.InventoryHelper;
import net.minecraft.item.BlockItemUseContext;
import net.minecraft.item.ItemStack;
import net.minecraft.state.BooleanProperty;
import net.minecraft.state.StateContainer.Builder;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.Direction;
import net.minecraft.util.Direction.AxisDirection;
import net.minecraft.util.Hand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.util.math.shapes.ISelectionContext;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.IWorld;
import net.minecraft.world.World;
import net.minecraftforge.fml.network.NetworkHooks;
public class FlexcrateBlock extends Block {
public class FlexcrateBlock extends ProperDirectionalBlock {
public static final BooleanProperty DOUBLE = BooleanProperty.create("double");
public FlexcrateBlock() {
super(Properties.from(Blocks.ANDESITE));
setDefaultState(getDefaultState().with(FACING, Direction.UP).with(DOUBLE, false));
}
@Override
@ -29,11 +40,75 @@ public class FlexcrateBlock extends Block {
return AllShapes.CRATE_BLOCK_SHAPE;
}
@Override
protected void fillStateContainer(Builder<Block, BlockState> builder) {
super.fillStateContainer(builder.add(DOUBLE));
}
@Override
public boolean hasTileEntity(BlockState state) {
return true;
}
@Override
public BlockState getStateForPlacement(BlockItemUseContext context) {
BlockPos pos = context.getPos();
World world = context.getWorld();
if (!context.isPlacerSneaking()) {
for (Direction d : Direction.values()) {
BlockState state = world.getBlockState(pos.offset(d));
if (AllBlocks.FLEXCRATE.typeOf(state) && !state.get(DOUBLE))
return getDefaultState().with(FACING, d).with(DOUBLE, true);
}
}
Direction placedOnFace = context.getFace().getOpposite();
BlockState state = world.getBlockState(pos.offset(placedOnFace));
if (AllBlocks.FLEXCRATE.typeOf(state) && !state.get(DOUBLE))
return getDefaultState().with(FACING, placedOnFace).with(DOUBLE, true);
return getDefaultState();
}
@Override
public void onBlockAdded(BlockState state, World worldIn, BlockPos pos, BlockState oldState, boolean isMoving) {
if (oldState.getBlock() != state.getBlock() && state.hasTileEntity() && state.get(DOUBLE)
&& state.get(FACING).getAxisDirection() == AxisDirection.POSITIVE) {
FlexcrateTileEntity te = (FlexcrateTileEntity) worldIn.getTileEntity(pos);
FlexcrateTileEntity other = te.getOtherCrate();
if (other == null)
return;
for (int slot = 0; slot < other.inventory.getSlots(); slot++) {
te.inventory.setStackInSlot(slot, other.inventory.getStackInSlot(slot));
other.inventory.setStackInSlot(slot, ItemStack.EMPTY);
}
te.allowedAmount = other.allowedAmount;
}
}
@Override
public BlockState updatePostPlacement(BlockState stateIn, Direction facing, BlockState facingState, IWorld worldIn,
BlockPos currentPos, BlockPos facingPos) {
boolean isDouble = stateIn.get(DOUBLE);
Direction blockFacing = stateIn.get(FACING);
boolean isFacingOther = AllBlocks.FLEXCRATE.typeOf(facingState) && facingState.get(DOUBLE)
&& facingState.get(FACING) == facing.getOpposite();
if (!isDouble) {
if (!isFacingOther)
return stateIn;
return stateIn.with(DOUBLE, true).with(FACING, facing);
}
if (facing != blockFacing)
return stateIn;
if (!isFacingOther)
return stateIn.with(DOUBLE, false);
return stateIn;
}
@Override
public boolean onBlockActivated(BlockState state, World worldIn, BlockPos pos, PlayerEntity player, Hand handIn,
BlockRayTraceResult hit) {
@ -41,9 +116,12 @@ public class FlexcrateBlock extends Block {
if (worldIn.isRemote) {
return true;
} else {
FlexcrateTileEntity te = (FlexcrateTileEntity) worldIn.getTileEntity(pos);
if (te != null)
NetworkHooks.openGui((ServerPlayerEntity) player, te, te::sendToContainer);
TileEntity te = worldIn.getTileEntity(pos);
if (te instanceof FlexcrateTileEntity) {
FlexcrateTileEntity fte = (FlexcrateTileEntity) te;
fte = fte.getMainCrate();
NetworkHooks.openGui((ServerPlayerEntity) player, fte, fte::sendToContainer);
}
return true;
}
}
@ -58,13 +136,9 @@ public class FlexcrateBlock extends Block {
if (worldIn.getTileEntity(pos) == null)
return;
FlexcrateTileEntity te = (FlexcrateTileEntity) worldIn.getTileEntity(pos);
for (int slot = 0; slot < te.inventory.getSlots(); slot++) {
InventoryHelper.spawnItemStack(worldIn, pos.getX(), pos.getY(), pos.getZ(),
te.inventory.getStackInSlot(slot));
}
if (state.hasTileEntity() && state.getBlock() != newState.getBlock()) {
FlexcrateTileEntity te = (FlexcrateTileEntity) worldIn.getTileEntity(pos);
te.onDestroyed();
worldIn.removeTileEntity(pos);
}

View file

@ -16,6 +16,7 @@ public class FlexcrateContainer extends Container {
public FlexcrateTileEntity te;
public PlayerInventory playerInventory;
public boolean doubleCrate;
public FlexcrateContainer(int id, PlayerInventory inv, PacketBuffer extraData) {
super(AllContainers.FLEXCRATE.type, id);
@ -34,9 +35,12 @@ public class FlexcrateContainer extends Container {
}
private void init() {
doubleCrate = te.isDoubleCrate();
int x = doubleCrate ? 52 : 124;
int maxCol = doubleCrate ? 8 : 4;
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
this.addSlot(new SlotItemHandler(te.inventory, col + row * 4, 124 + col * 18, 25 + row * 18));
for (int col = 0; col < maxCol; ++col) {
this.addSlot(new SlotItemHandler(te.inventory, col + row * maxCol, x + col * 18, 25 + row * 18));
}
}
@ -66,8 +70,7 @@ public class FlexcrateContainer extends Container {
if (index < 16) {
mergeItemStack(stack, 16, inventorySlots.size(), false);
te.inventory.onContentsChanged(index);
}
else
} else
mergeItemStack(stack, 0, 15, false);
return ItemStack.EMPTY;

View file

@ -1,6 +1,7 @@
package com.simibubi.create.modules.logistics.block.inventories;
import static com.simibubi.create.ScreenResources.FLEXCRATE;
import static com.simibubi.create.ScreenResources.FLEXCRATE_DOUBLE;
import static com.simibubi.create.ScreenResources.PLAYER_INVENTORY;
import java.util.ArrayList;
@ -47,9 +48,9 @@ public class FlexcrateScreen extends AbstractSimiContainerScreen<FlexcrateContai
widgets.clear();
allowedItemsLabel = new Label(guiLeft + 100 + 70, guiTop + 107, "").colored(0xD3CBBE).withShadow();
allowedItems = new ScrollInput(guiLeft + 100 + 65, guiTop + 104, 41, 14).titled(storageSpace).withRange(1, 1025)
.writingTo(allowedItemsLabel).withShiftStep(64).setState(te.allowedAmount)
.calling(s -> lastModification = 0);
allowedItems = new ScrollInput(guiLeft + 100 + 65, guiTop + 104, 41, 14).titled(storageSpace)
.withRange(1, (container.doubleCrate ? 2049 : 1025)).writingTo(allowedItemsLabel).withShiftStep(64)
.setState(te.allowedAmount).calling(s -> lastModification = 0);
allowedItems.onChanged();
widgets.add(allowedItemsLabel);
widgets.add(allowedItems);
@ -67,20 +68,26 @@ public class FlexcrateScreen extends AbstractSimiContainerScreen<FlexcrateContai
int hFontColor = 0xD3CBBE;
int fontColor = 0x4B3A22;
if (container.doubleCrate) {
crateLeft -= 72;
FLEXCRATE_DOUBLE.draw(this, crateLeft, crateTop);
} else
FLEXCRATE.draw(this, crateLeft, crateTop);
font.drawStringWithShadow(title, crateLeft - 3 + (FLEXCRATE.width - font.getStringWidth(title)) / 2,
crateTop + 10, hFontColor);
String itemCount = "" + te.itemCount;
font.drawString(itemCount, crateLeft + 53 - font.getStringWidth(itemCount), crateTop + 107, fontColor);
font.drawString(itemCount, guiLeft + 100 + 53 - font.getStringWidth(itemCount), crateTop + 107, fontColor);
PLAYER_INVENTORY.draw(this, invLeft, invTop);
font.drawString(playerInventory.getDisplayName().getFormattedText(), invLeft + 7, invTop + 6, 0x666666);
for (int slot = 0; slot < 16; slot++) {
for (int slot = 0; slot < (container.doubleCrate ? 32 : 16); slot++) {
if (allowedItems.getState() > slot * 64)
continue;
int x = crateLeft + 23 + (slot % 4) * 18;
int y = crateTop + 24 + (slot / 4) * 18;
int slotsPerRow = (container.doubleCrate ? 8 : 4);
int x = crateLeft + 23 + (slot % slotsPerRow) * 18;
int y = crateTop + 24 + (slot / slotsPerRow) * 18;
ScreenResources.FLEXCRATE_LOCKED_SLOT.draw(this, x, y);
}
@ -88,7 +95,8 @@ public class FlexcrateScreen extends AbstractSimiContainerScreen<FlexcrateContai
// to see or debug the bounds of the extra area uncomment the following lines
// Rectangle2d r = extraAreas.get(0);
//fill(r.getX() + r.getWidth(), r.getY() + r.getHeight(), r.getX(), r.getY(), 0xd3d3d3d3);
// fill(r.getX() + r.getWidth(), r.getY() + r.getHeight(), r.getX(), r.getY(),
// 0xd3d3d3d3);
}
@Override
@ -106,6 +114,9 @@ public class FlexcrateScreen extends AbstractSimiContainerScreen<FlexcrateContai
lastModification = -1;
AllPackets.channel.sendToServer(new ConfigureFlexcratePacket(te.getPos(), allowedItems.getState()));
}
if (container.doubleCrate != te.isDoubleCrate())
container.playerInventory.player.closeScreen();
}
public BlockState getRenderedBlock() {

View file

@ -1,17 +1,21 @@
package com.simibubi.create.modules.logistics.block.inventories;
import com.simibubi.create.AllBlocks;
import com.simibubi.create.AllTileEntities;
import com.simibubi.create.foundation.block.SyncedTileEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.InventoryHelper;
import net.minecraft.inventory.container.Container;
import net.minecraft.inventory.container.INamedContainerProvider;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.network.PacketBuffer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.Direction;
import net.minecraft.util.Direction.AxisDirection;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraftforge.common.capabilities.Capability;
@ -24,7 +28,7 @@ public class FlexcrateTileEntity extends SyncedTileEntity implements INamedConta
public class Inv extends ItemStackHandler {
public Inv() {
super(16);
super(32);
}
@Override
@ -77,17 +81,70 @@ public class FlexcrateTileEntity extends SyncedTileEntity implements INamedConta
return new FlexcrateContainer(id, inventory, this);
}
public boolean isDoubleCrate() {
return getBlockState().get(FlexcrateBlock.DOUBLE);
}
public FlexcrateTileEntity getMainCrate() {
if (isDoubleCrate() && getFacing().getAxisDirection() == AxisDirection.NEGATIVE)
return getOtherCrate();
return this;
}
public FlexcrateTileEntity getOtherCrate() {
if (!AllBlocks.FLEXCRATE.typeOf(getBlockState()))
return null;
TileEntity tileEntity = world.getTileEntity(pos.offset(getFacing()));
if (tileEntity instanceof FlexcrateTileEntity)
return (FlexcrateTileEntity) tileEntity;
return null;
}
public Direction getFacing() {
return getBlockState().get(FlexcrateBlock.FACING);
}
public void onDestroyed() {
FlexcrateTileEntity other = getOtherCrate();
if (other == null) {
for (int slot = 0; slot < inventory.getSlots(); slot++)
drop(slot);
return;
}
FlexcrateTileEntity main = getMainCrate();
if (this == main) {
for (int slot = 0; slot < inventory.getSlots(); slot++) {
other.inventory.setStackInSlot(slot, inventory.getStackInSlot(slot));
inventory.setStackInSlot(slot, ItemStack.EMPTY);
}
other.allowedAmount = Math.min(1024, allowedAmount);
}
for (int slot = 16; slot < other.inventory.getSlots(); slot++)
other.drop(slot);
}
private void drop(int slot) {
InventoryHelper.spawnItemStack(world, pos.getX(), pos.getY(), pos.getZ(), inventory.getStackInSlot(slot));
}
@Override
public CompoundNBT write(CompoundNBT compound) {
if (getMainCrate() == this) {
compound.putBoolean("Main", true);
compound.putInt("AllowedAmount", allowedAmount);
compound.put("Inventory", inventory.serializeNBT());
}
return super.write(compound);
}
@Override
public void read(CompoundNBT compound) {
if (compound.contains("Main")) {
allowedAmount = compound.getInt("AllowedAmount");
inventory.deserializeNBT(compound.getCompound("Inventory"));
}
super.read(compound);
}
@ -110,7 +167,7 @@ public class FlexcrateTileEntity extends SyncedTileEntity implements INamedConta
@Override
public <T> LazyOptional<T> getCapability(Capability<T> capability, Direction facing) {
if (capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
return invHandler.cast();
return getMainCrate().invHandler.cast();
return super.getCapability(capability, facing);
}

View file

@ -1,5 +1,17 @@
{
"variants": {
"": { "model": "create:block/flex_crate" }
"double=true,facing=up": { "model": "create:block/flexcrate/double_bottom" },
"double=true,facing=down": { "model": "create:block/flexcrate/double_top" },
"double=true,facing=west": { "model": "create:block/flexcrate/double_left", "y": 90 },
"double=true,facing=east": { "model": "create:block/flexcrate/double_right", "y": 90 },
"double=true,facing=north": { "model": "create:block/flexcrate/double_right" },
"double=true,facing=south": { "model": "create:block/flexcrate/double_left" },
"double=false,facing=up": { "model": "create:block/flexcrate/single" },
"double=false,facing=down": { "model": "create:block/flexcrate/single" },
"double=false,facing=west": { "model": "create:block/flexcrate/single" },
"double=false,facing=east": { "model": "create:block/flexcrate/single" },
"double=false,facing=north": { "model": "create:block/flexcrate/single" },
"double=false,facing=south": { "model": "create:block/flexcrate/single" }
}
}

View file

@ -110,7 +110,7 @@
"block.create.contact": "Redstone Contact",
"block.create.redstone_bridge": "Redstone Link",
"block.create.stockswitch": "Stockpile Switch",
"block.create.flexcrate": "FlexCrate",
"block.create.flexcrate": "Adjustable Crate",
"block.create.extractor": "Extractor",
"block.create.belt_funnel": "Funnel",
"block.create.linked_extractor": "Linked Extractor",
@ -291,7 +291,7 @@
"create.logistics.firstFrequency": "Freq. #1",
"create.logistics.secondFrequency": "Freq. #2",
"create.gui.flexcrate.title": "FlexCrate",
"create.gui.flexcrate.title": "Adjustable Crate",
"create.gui.flexcrate.storageSpace": "Storage Space",
"create.gui.stockswitch.title": "Stockpile Switch",
@ -737,7 +737,7 @@
"block.create.contact.tooltip.condition2": "When moved by Mechanical Piston",
"block.create.contact.tooltip.behaviour2": "Triggers all stationary Contacts passing by",
"block.create.flexcrate.tooltip": "FLEXCRATE",
"block.create.flexcrate.tooltip": "ADJUSTABLE CRATE",
"block.create.flexcrate.tooltip.summary": "This _Storage_ _Container_ allows Manual control over its capacity. It can hold up to _16_ _Stacks_ of any Item",
"block.create.flexcrate.tooltip.control1": "When R-Clicked",
"block.create.flexcrate.tooltip.action1": "Opens the _Interface_",

View file

@ -0,0 +1,22 @@
{
"credit": "Made with Blockbench",
"parent": "block/block",
"textures": {
"0": "create:block/double_flexcrate_side",
"3": "create:block/brass_casing_14",
"particle": "create:block/flex_crate"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 16, 15],
"faces": {
"north": {"uv": [8.5, 8, 15.5, 16], "texture": "#0"},
"east": {"uv": [8.5, 8, 15.5, 16], "texture": "#0"},
"south": {"uv": [8.5, 8, 15.5, 16], "texture": "#0"},
"west": {"uv": [8.5, 8, 15.5, 16], "texture": "#0"},
"down": {"uv": [1, 1, 15, 15], "rotation": 270, "texture": "#3"}
}
}
]
}

View file

@ -0,0 +1,23 @@
{
"credit": "Made with Blockbench",
"parent": "block/block",
"textures": {
"0": "create:block/double_flexcrate_side",
"1": "create:block/double_flexcrate_bottom",
"2": "create:block/flex_crate",
"particle": "create:block/flex_crate"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 14, 16],
"faces": {
"north": {"uv": [1, 1, 15, 15], "texture": "#2"},
"east": {"uv": [8, 0.5, 15.5, 7.5], "texture": "#0"},
"west": {"uv": [0.5, 0.5, 8, 7.5], "texture": "#0"},
"up": {"uv": [0.5, 0.5, 8, 7.5], "rotation": 90, "texture": "#1"},
"down": {"uv": [0.5, 0.5, 8, 7.5], "rotation": 270, "texture": "#1"}
}
}
]
}

View file

@ -0,0 +1,23 @@
{
"credit": "Made with Blockbench",
"parent": "block/block",
"textures": {
"0": "create:block/double_flexcrate_side",
"1": "create:block/double_flexcrate_bottom",
"2": "create:block/flex_crate",
"particle": "create:block/flex_crate"
},
"elements": [
{
"from": [1, 0, 0],
"to": [15, 14, 15],
"faces": {
"east": {"uv": [0.5, 0.5, 8, 7.5], "texture": "#0"},
"south": {"uv": [1, 1, 15, 15], "texture": "#2"},
"west": {"uv": [8, 0.5, 15.5, 7.5], "texture": "#0"},
"up": {"uv": [8, 0.5, 15.5, 7.5], "rotation": 90, "texture": "#1"},
"down": {"uv": [8, 0.5, 15.5, 7.5], "rotation": 270, "texture": "#1"}
}
}
]
}

View file

@ -0,0 +1,22 @@
{
"credit": "Made with Blockbench",
"parent": "block/block",
"textures": {
"0": "create:block/double_flexcrate_side",
"3": "create:block/brass_casing_14",
"particle": "create:block/flex_crate"
},
"elements": [
{
"from": [1, 0, 1],
"to": [15, 14, 15],
"faces": {
"north": {"uv": [0.5, 8.5, 7.5, 15.5], "texture": "#0"},
"east": {"uv": [0.5, 8.5, 7.5, 15.5], "texture": "#0"},
"south": {"uv": [0.5, 8.5, 7.5, 15.5], "texture": "#0"},
"west": {"uv": [0.5, 8.5, 7.5, 15.5], "texture": "#0"},
"up": {"uv": [1, 1, 15, 15], "rotation": 90, "texture": "#3"}
}
}
]
}

View file

@ -1,3 +1,3 @@
{
"parent": "create:block/flex_crate"
"parent": "create:block/flexcrate/single"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB