Allow any book to be placed in a bookshelf

- Modifies the bookshelf model and blockstate to choose a texture dynamically based on the book item
- Changes bookshelf slots to accept all books from wizardry and vanilla minecraft by default
- Adds a couple of hooks to allow addons to register their own book items and model textures
- Adds a config option to allow modpack makers to add more book items
This commit is contained in:
Electroblob77
2020-05-15 22:52:32 +01:00
parent 28da6810d7
commit 48a350b63a
15 changed files with 186 additions and 41 deletions
@@ -296,6 +296,8 @@ public final class Settings {
Wizardry.MODID + ":acacia_bookshelf",
Wizardry.MODID + ":dark_oak_bookshelf"
);
/** <b>[Synchronised]</b> List of registry names of items that can be placed in a bookshelf. */
public Pair<ResourceLocation, Short>[] bookItems = parseItemMetaStrings();
// Client-only settings. These settings only affect client-side code and hence are not synced. Each client obeys
// its own values for these, and changing them on a dedicated server will have no effect.
@@ -735,6 +737,11 @@ public final class Settings {
bookshelfBlocks = parseItemMetaStrings(property.getStringList());
propOrder.add(property.getName());
property = config.get(TWEAKS_CATEGORY, "bookItems", new String[0], "List of registry names of items which can be placed in a bookshelf, in addition to the defaults. Item names are not case sensitive. For mod items, prefix with the mod ID (e.g. thaumcraft:thaumonomicon).");
property.setLanguageKey("config." + Wizardry.MODID + ".book_items");
bookItems = parseItemMetaStrings(property.getStringList());
propOrder.add(property.getName());
property = config.get(TWEAKS_CATEGORY, "bookshelfSearchRadius", 4,
"The maximum number of blocks a bookshelf can be from an arcane workbench or lectern to be able to link to it.",
1, 10);
@@ -1,5 +1,6 @@
package electroblob.wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.command.CommandCastSpell;
import electroblob.wizardry.command.CommandDiscoverSpell;
import electroblob.wizardry.command.CommandSetAlly;
@@ -8,6 +9,7 @@ import electroblob.wizardry.data.DispenserCastingData;
import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.integration.antiqueatlas.WizardryAntiqueAtlasIntegration;
import electroblob.wizardry.integration.baubles.WizardryBaublesIntegration;
import electroblob.wizardry.inventory.ContainerBookshelf;
import electroblob.wizardry.misc.Forfeit;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.*;
@@ -123,6 +125,7 @@ public class Wizardry {
WizardryLoot.register();
WizardryAdvancementTriggers.register();
Forfeit.register();
BlockBookshelf.registerStandardBookModelTextures();
// Client-side stuff (via proxies)
proxy.registerRenderers();
@@ -159,6 +162,8 @@ public class Wizardry {
WizardryPacketHandler.initPackets();
// Post-registry extras
BlockBookshelf.compileBookModelTextures();
ContainerBookshelf.initDefaultBookItems();
WizardryItems.populateWandMap();
WizardryItems.populateArmourMap();
WizardryItems.registerDispenseBehaviours();
@@ -1,28 +1,29 @@
package electroblob.wizardry.block;
import com.google.common.collect.ImmutableList;
import electroblob.wizardry.Settings;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.WizardryGuiHandler;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.registry.WizardryTabs;
import electroblob.wizardry.tileentity.TileEntityBookshelf;
import net.minecraft.block.BlockHorizontal;
import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.SoundType;
import net.minecraft.block.material.Material;
import net.minecraft.block.properties.PropertyBool;
import net.minecraft.block.properties.PropertyInteger;
import net.minecraft.block.state.BlockFaceShape;
import net.minecraft.block.state.BlockStateContainer;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Items;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.InventoryHelper;
import net.minecraft.item.Item;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.*;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.IWorldEventListener;
@@ -31,12 +32,17 @@ import net.minecraftforge.common.property.IExtendedBlockState;
import net.minecraftforge.common.property.Properties;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.apache.commons.lang3.ArrayUtils;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
@Mod.EventBusSubscriber
public class BlockBookshelf extends BlockHorizontal implements ITileEntityProvider {
@@ -46,13 +52,11 @@ public class BlockBookshelf extends BlockHorizontal implements ITileEntityProvid
public static final double PLAYER_NOTIFY_RANGE = 32;
public static final int SLOT_COUNT = 12;
public static final UnlistedPropertyBool[] BOOKS = new UnlistedPropertyBool[SLOT_COUNT];
public static final UnlistedPropertyInt[] BOOKS = new UnlistedPropertyInt[SLOT_COUNT];
static {
for(int i=0; i<SLOT_COUNT; i++){
BOOKS[i] = new UnlistedPropertyBool("book" + i);
}
}
private static final Map<Supplier<Item>, ResourceLocation> BOOK_TEXTURE_MAP = new HashMap<>();
private static ImmutableList<Item> bookItems;
private static ImmutableList<ResourceLocation> bookTextures;
public BlockBookshelf(){
super(Material.WOOD);
@@ -115,9 +119,19 @@ public class BlockBookshelf extends BlockHorizontal implements ITileEntityProvid
IExtendedBlockState s = (IExtendedBlockState)super.getExtendedState(state, world, pos);
if(world.getTileEntity(pos) instanceof TileEntityBookshelf){
TileEntityBookshelf tileentity = ((TileEntityBookshelf)world.getTileEntity(pos));
for(int i = 0; i < tileentity.getSizeInventory(); i++){
s = s.withProperty(BOOKS[i], !tileentity.getStackInSlot(i).isEmpty());
if(tileentity.getStackInSlot(i).isEmpty()){
s = s.withProperty(BOOKS[i], bookItems.size());
}else{
Item item = tileentity.getStackInSlot(i).getItem();
// Default to the standard spell book texture, which will always be the first item in the list
s = s.withProperty(BOOKS[i], bookItems.contains(item) ? bookItems.indexOf(item) : 0);
}
}
}
@@ -176,14 +190,71 @@ public class BlockBookshelf extends BlockHorizontal implements ITileEntityProvid
return tileentity != null && tileentity.receiveClientEvent(id, param);
}
// Copied from BlockFluidBase, only reason it exists is because of java's weird restrictions on generics
private static final class UnlistedPropertyBool extends Properties.PropertyAdapter<Boolean> {
/** Returns the list of book textures, in ID order. */
public static ImmutableList<ResourceLocation> getBookTextures(){
// ModelBookshelf calls this before init() so we need to return the map values as a fallback
return bookTextures == null ? ImmutableList.copyOf(BOOK_TEXTURE_MAP.values()) : bookTextures;
}
public UnlistedPropertyBool(String name){
super(PropertyBool.create(name));
/** Called during block registration to initialise the block properties for each book. */
public static void initBookProperties(){
for(int i=0; i<SLOT_COUNT; i++){
BOOKS[i] = new UnlistedPropertyInt("book" + i);
}
}
// Based on BlockFluid's UnlistedPropertyBool; this is only needed because of Java's weird restrictions on generics
private static final class UnlistedPropertyInt extends Properties.PropertyAdapter<Integer> {
public UnlistedPropertyInt(String name){
// Max is inclusive here, but we're using the largest value for no book so there's an extra one (can't use -1)
super(PropertyInteger.create(name, 0, BOOK_TEXTURE_MAP.size()));
}
}
/** Called from {@link Wizardry#init(FMLInitializationEvent)} to retrieve the actual book items and compile them
* and their textures into immutable lists. */
public static void compileBookModelTextures(){
ImmutableList.Builder<Item> itemListBuider = ImmutableList.builder();
ImmutableList.Builder<ResourceLocation> textureListBuilder = ImmutableList.builder();
for(Map.Entry<Supplier<Item>, ResourceLocation> entry : BOOK_TEXTURE_MAP.entrySet()){
itemListBuider.add(entry.getKey().get());
textureListBuilder.add(entry.getValue());
}
bookItems = itemListBuider.build();
bookTextures = textureListBuilder.build();
}
/** Called from {@link Wizardry#preInit(FMLPreInitializationEvent)} to register the default set of book textures. */
public static void registerStandardBookModelTextures(){
// Vanilla Minecraft books
// Regular brown books are the default so they're registered first
registerBookModelTexture(() -> Items.BOOK, new ResourceLocation(Wizardry.MODID, "blocks/books_brown"));
registerBookModelTexture(() -> Items.WRITABLE_BOOK, new ResourceLocation(Wizardry.MODID, "blocks/books_brown"));
registerBookModelTexture(() -> Items.WRITTEN_BOOK, new ResourceLocation(Wizardry.MODID, "blocks/books_brown"));
registerBookModelTexture(() -> Items.ENCHANTED_BOOK, new ResourceLocation(Wizardry.MODID, "blocks/books_enchanted"));
// Wizardry books
registerBookModelTexture(() -> WizardryItems.spell_book, new ResourceLocation(Wizardry.MODID, "blocks/books_red"));
registerBookModelTexture(() -> WizardryItems.wizard_handbook, new ResourceLocation(Wizardry.MODID, "blocks/books_blue"));
registerBookModelTexture(() -> WizardryItems.arcane_tome, new ResourceLocation(Wizardry.MODID, "blocks/books_purple"));
}
/**
* Registers a book texture to be used for the book model when the given item is placed in a bookshelf. This method
* <b>must</b> be called from {@code preInit} as the registered item count is required during block registration.
* This also necessitates the use of a supplier to fetch the item, since the items will not yet be registered.
* @param itemFactory A {@link Supplier} that returns the book item to link the texture to. This item need not
* be an instance of {@link electroblob.wizardry.item.ItemSpellBook ItemSpellBook}. Duplicate
* items will result in an {@link IllegalArgumentException} being thrown later.
* @param texture The texture to apply to the book model.
*/
public static void registerBookModelTexture(Supplier<Item> itemFactory, ResourceLocation texture){
BOOK_TEXTURE_MAP.put(itemFactory, texture);
}
/**
* Returns a list of nearby bookshelves' inventories, where 'bookshelves' are any tile entities with inventories
* whose blocks are specified in the config file under the {@code bookshelfBlocks} option.
@@ -17,9 +17,9 @@ import java.util.List;
public class BakedModelBookshelf implements IBakedModel {
private final IBakedModel bookshelf;
private final IBakedModel[] books;
private final IBakedModel[][] books;
public BakedModelBookshelf(IBakedModel bookshelf, IBakedModel... books){
public BakedModelBookshelf(IBakedModel bookshelf, IBakedModel[][] books){
this.bookshelf = bookshelf;
this.books = books;
}
@@ -36,9 +36,9 @@ public class BakedModelBookshelf implements IBakedModel {
IExtendedBlockState extendedState = (IExtendedBlockState)state;
for(int i = 0; i<BlockBookshelf.SLOT_COUNT; i++){
Boolean value = extendedState.getValue(BlockBookshelf.BOOKS[i]);
if(value == null) return fallback.getQuads(null, side, rand);
if(value) quads.addAll(books[i].getQuads(state, side, rand));
Integer value = extendedState.getValue(BlockBookshelf.BOOKS[i]);
if(value == null || value < 0) return fallback.getQuads(null, side, rand);
if(value < books.length) quads.addAll(books[value][i].getQuads(state, side, rand)); // Empty slots use books.length
}
return quads;
@@ -1,6 +1,7 @@
package electroblob.wizardry.client.model;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import net.minecraft.client.renderer.block.model.IBakedModel;
@@ -19,7 +20,7 @@ import java.util.function.Function;
public class ModelBookshelf implements IModel {
private static final ResourceLocation BOOK_TEXTURE = new ResourceLocation(Wizardry.MODID, "blocks/books");
private static final ResourceLocation DEFAULT_BOOK_TEXTURE = new ResourceLocation(Wizardry.MODID, "blocks/books");
private static final List<ResourceLocation> bookModelLocations = new ArrayList<>();
@@ -48,11 +49,18 @@ public class ModelBookshelf implements IModel {
// I don't know why default state works here (surely it ought to be the state param?), but it works so who cares
IBakedModel bookshelf = bookshelfModel.bake(bookshelfModel.getDefaultState(), format, bakedTextureGetter);
IBakedModel[] books = new IBakedModel[BlockBookshelf.SLOT_COUNT];
ImmutableList<ResourceLocation> textures = BlockBookshelf.getBookTextures();
for(int i = 0; i < BlockBookshelf.SLOT_COUNT; i++){
IModel bookModel = ModelLoaderRegistry.getModel(new ModelResourceLocation(bookModelLocations.get(i), variant));
books[i] = bookModel.bake(bookModel.getDefaultState(), format, bakedTextureGetter); // Same here!
IBakedModel[][] books = new IBakedModel[textures.size()][BlockBookshelf.SLOT_COUNT];
for(int i = 0; i < textures.size(); i++){
ImmutableMap<String, String> retexturer = ImmutableMap.of("books", textures.get(i).toString());
for(int j = 0; j < BlockBookshelf.SLOT_COUNT; j++){
IModel bookModel = ModelLoaderRegistry.getModel(new ModelResourceLocation(bookModelLocations.get(j), variant)).retexture(retexturer);
books[i][j] = bookModel.bake(bookModel.getDefaultState(), format, bakedTextureGetter); // Same here!
}
}
// To clarify: the whole point of doing this was to avoid having to bake 4 * 2^12 models per bookshelf type
@@ -85,7 +93,7 @@ public class ModelBookshelf implements IModel {
// 3. Is it correct to use getModel() to get the dependency models upon construction of this class so I can
// access their textures properly? Or is that too early?
// 4. If so, what's the point of getDependencies, and why should I override it?
return ImmutableList.of(BOOK_TEXTURE);
return BlockBookshelf.getBookTextures();// ImmutableList.of(DEFAULT_BOOK_TEXTURE);
}
}
@@ -1,17 +1,29 @@
package electroblob.wizardry.inventory;
import electroblob.wizardry.Settings;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.block.BlockBookshelf;
import electroblob.wizardry.item.ItemSpellBook;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.tileentity.TileEntityBookshelf;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Items;
import net.minecraft.inventory.Container;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.Slot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
public class ContainerBookshelf extends Container {
private static final Set<Item> validItems = new HashSet<>();
/** The bookshelf tile entity associated with this container. */
public TileEntityBookshelf tileentity;
@@ -21,7 +33,7 @@ public class ContainerBookshelf extends Container {
for(int y = 0; y < 2; y++){
for(int x = 0; x < BlockBookshelf.SLOT_COUNT / 2; x++){
this.addSlotToContainer(new SlotBookshelf(tileentity, x + BlockBookshelf.SLOT_COUNT / 2 * y, 35 + x * 18, 17 + y * 18, 64, ItemSpellBook.class));
this.addSlotToContainer(new SlotBookshelf(tileentity, x + BlockBookshelf.SLOT_COUNT / 2 * y, 35 + x * 18, 17 + y * 18));
}
}
@@ -94,11 +106,31 @@ public class ContainerBookshelf extends Container {
return remainder;
}
public class SlotBookshelf extends SlotItemClassList {
/**
* Adds the given item to the set of items that can be put in a bookshelf. This method should be called from the
* {@code init()} phase.
* @param item The item to register
* @see BlockBookshelf#registerBookModelTexture(Supplier, ResourceLocation)
*/
public static void registerBookItem(Item item){
validItems.add(item);
}
@SafeVarargs
public SlotBookshelf(IInventory inventory, int index, int x, int y, int stackLimit, Class<? extends Item>... allowedItemClasses){
super(inventory, index, x, y, stackLimit, allowedItemClasses);
/** Called from {@link Wizardry#init(FMLInitializationEvent)} to register the default book items. */
public static void initDefaultBookItems(){
registerBookItem(WizardryItems.spell_book);
registerBookItem(WizardryItems.arcane_tome);
registerBookItem(WizardryItems.wizard_handbook);
registerBookItem(Items.BOOK);
registerBookItem(Items.WRITTEN_BOOK);
registerBookItem(Items.WRITABLE_BOOK);
registerBookItem(Items.ENCHANTED_BOOK);
}
public class SlotBookshelf extends Slot {
public SlotBookshelf(IInventory inventory, int index, int x, int y){
super(inventory, index, x, y);
}
@Override
@@ -115,6 +147,10 @@ public class ContainerBookshelf extends Container {
return result;
}
@Override
public boolean isItemValid(ItemStack stack){
return validItems.contains(stack.getItem()) || Settings.containsMetaItem(Wizardry.settings.bookItems, stack);
}
}
}
@@ -41,6 +41,7 @@ public class PacketSyncSettings implements IMessageHandler<Message, IMessage> {
Wizardry.settings.forfeitChance = message.settings.forfeitChance;
Wizardry.settings.bookshelfSearchRadius = message.settings.bookshelfSearchRadius;
Wizardry.settings.bookshelfBlocks = message.settings.bookshelfBlocks;
Wizardry.settings.bookItems = message.settings.bookItems;
}
public static class Message implements IMessage {
@@ -57,7 +58,6 @@ public class PacketSyncSettings implements IMessageHandler<Message, IMessage> {
}
@Override
@SuppressWarnings("unchecked")
public void fromBytes(ByteBuf buf){
// I'm guessing the settings field will be null here, so it needs initialising.
// This is also a great reason to have the settings as an actual object.
@@ -69,12 +69,8 @@ public class PacketSyncSettings implements IMessageHandler<Message, IMessage> {
settings.replaceVanillaFireballs = buf.readBoolean();
settings.forfeitChance = buf.readFloat();
settings.bookshelfSearchRadius = buf.readInt();
int length = buf.readInt();
List<Pair<ResourceLocation, Short>> entries = new ArrayList<>();
for(int i=0; i<length; i++){
entries.add(Pair.of(new ResourceLocation(ByteBufUtils.readUTF8String(buf)), buf.readShort()));
}
settings.bookshelfBlocks = entries.toArray(new Pair[0]);
settings.bookshelfBlocks = readMetaItems(buf);
settings.bookItems = readMetaItems(buf);
}
@Override
@@ -85,11 +81,27 @@ public class PacketSyncSettings implements IMessageHandler<Message, IMessage> {
buf.writeBoolean(settings.replaceVanillaFireballs);
buf.writeFloat((float)settings.forfeitChance); // Configs don't have floats but this can only be 0-1 anyway
buf.writeInt(settings.bookshelfSearchRadius);
buf.writeInt(settings.bookshelfBlocks.length);
for(Pair<ResourceLocation, Short> entry : settings.bookshelfBlocks){
writeMetaItems(buf, settings.bookshelfBlocks);
writeMetaItems(buf, settings.bookItems);
}
@SuppressWarnings("unchecked")
private static Pair<ResourceLocation, Short>[] readMetaItems(ByteBuf buf){
int length = buf.readInt();
List<Pair<ResourceLocation, Short>> entries = new ArrayList<>();
for(int i=0; i<length; i++){
entries.add(Pair.of(new ResourceLocation(ByteBufUtils.readUTF8String(buf)), buf.readShort()));
}
return entries.toArray(new Pair[0]);
}
private static void writeMetaItems(ByteBuf buf, Pair<ResourceLocation, Short>[] items){
buf.writeInt(items.length);
for(Pair<ResourceLocation, Short> entry : items){
ByteBufUtils.writeUTF8String(buf, entry.getLeft().toString());
buf.writeShort(entry.getRight());
}
}
}
}
@@ -96,6 +96,8 @@ public final class WizardryBlocks {
@SubscribeEvent
public static void register(RegistryEvent.Register<Block> event){
BlockBookshelf.initBookProperties();
IForgeRegistry<Block> registry = event.getRegistry();
registerBlock(registry, "arcane_workbench", new BlockArcaneWorkbench().setHardness(1.0F).setCreativeTab(WizardryTabs.WIZARDRY));
@@ -1238,6 +1238,8 @@ config.ebwizardry.bow_item_whitelist=Bow Item Whitelist
config.ebwizardry.bow_item_whitelist.tooltip=List of registry names of items which should count as bows for imbuement spells. Most bows should work automatically, but those that don't can be added manually here. Item names are not case sensitive. For mod items, prefix with the mod ID (e.g. tconstruct:shortbow).
config.ebwizardry.bookshelf_blocks=Bookshelf Blocks
config.ebwizardry.bookshelf_blocks.tooltip=List of registry names of blocks that count as bookshelves for the arcane workbench and lectern. Block names are not case sensitive. For mod blocks, prefix with the mod ID (e.g. ebwizardry:oak_bookshelf).
config.ebwizardry.book_items=Book Items
config.ebwizardry.book_items.tooltip=List of registry names of items which can be placed in a bookshelf, in addition to the defaults. Item names are not case sensitive. For mod items, prefix with the mod ID (e.g. thaumcraft:thaumonomicon).
config.ebwizardry.bookshelf_search_radius=Bookshelf Search Radius
config.ebwizardry.bookshelf_search_radius.tooltip=The maximum number of blocks a bookshelf can be from an arcane workbench or lectern to be able to link to it.
config.ebwizardry.currency_items=Currency Items
@@ -1238,6 +1238,8 @@ config.ebwizardry.bow_item_whitelist=Bow Item Whitelist
config.ebwizardry.bow_item_whitelist.tooltip=List of registry names of items which should count as bows for imbuement spells. Most bows should work automatically, but those that don't can be added manually here. Item names are not case sensitive. For mod items, prefix with the mod ID (e.g. tconstruct:shortbow).
config.ebwizardry.bookshelf_blocks=Bookshelf Blocks
config.ebwizardry.bookshelf_blocks.tooltip=List of registry names of blocks that count as bookshelves for the arcane workbench and lectern. Block names are not case sensitive. For mod blocks, prefix with the mod ID (e.g. ebwizardry:oak_bookshelf).
config.ebwizardry.book_items=Book Items
config.ebwizardry.book_items.tooltip=List of registry names of items which can be placed in a bookshelf, in addition to the defaults. Item names are not case sensitive. For mod items, prefix with the mod ID (e.g. thaumcraft:thaumonomicon).
config.ebwizardry.bookshelf_search_radius=Bookshelf Search Radius
config.ebwizardry.bookshelf_search_radius.tooltip=The maximum number of blocks a bookshelf can be from an arcane workbench or lectern to be able to link to it.
config.ebwizardry.currency_items=Currency Items
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B