Convert lecterns into Rivencraft book stands
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 07:00:25 -04:00
1 parent 0e602f0154
commit 3109938a97
15 files changed
+390 -8

No files matched your search

@@ -4,8 +4,12 @@ import com.mojang.logging.LogUtils;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;
import org.slf4j.Logger;
@Mod(Rivencraft.MOD_ID)
@@ -15,8 +19,11 @@ public final class Rivencraft {
public Rivencraft(IEventBus modEventBus) {
ModDataComponents.register(modEventBus);
ModBlocks.register(modEventBus);
ModBlockEntities.register(modEventBus);
ModItems.register(modEventBus);
ModCreativeTabs.register(modEventBus);
NeoForge.EVENT_BUS.addListener(BookStandConversionHandler::onRightClickBlock);
LOGGER.info("Initializing Rivencraft");
}
}
@@ -0,0 +1,91 @@
package net.dnorberg.rivencraft.block;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
/** A lectern-compatible stand that safely stores and activates Rivencraft books. */
public final class BookStandBlock extends LecternBlock {
public static final MapCodec<LecternBlock> CODEC = simpleCodec(BookStandBlock::new);
public BookStandBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
public MapCodec<LecternBlock> codec() {
return CODEC;
}
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new BookStandBlockEntity(pos, state);
}
@Override
protected InteractionResult useWithoutItem(
BlockState state,
Level level,
BlockPos pos,
Player player,
BlockHitResult hitResult
) {
if (!(level.getBlockEntity(pos) instanceof BookStandBlockEntity stand)) {
return InteractionResult.PASS;
}
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
ItemStack book = stand.takeBook();
BlockState lectern = Blocks.LECTERN.defaultBlockState()
.setValue(FACING, state.getValue(FACING));
level.setBlock(pos, lectern, 3);
if (!player.getInventory().add(book)) {
player.drop(book, false);
}
level.playSound(null, pos, SoundEvents.BOOK_PAGE_TURN, SoundSource.BLOCKS, 1.0F, 1.0F);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
ItemStack book = stand.getBook();
if (book.getItem() instanceof LinkingBookItem && player instanceof ServerPlayer serverPlayer) {
LinkingBookItem.useBoundBook(serverPlayer, book);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Override
protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
if (!state.is(newState.getBlock()) && level.getBlockEntity(pos) instanceof BookStandBlockEntity stand) {
ItemStack book = stand.takeBook();
if (!book.isEmpty()) {
ItemEntity droppedBook = new ItemEntity(
level,
pos.getX() + 0.5,
pos.getY() + 1.0,
pos.getZ() + 0.5,
book
);
droppedBook.setDefaultPickUpDelay();
level.addFreshEntity(droppedBook);
}
}
super.onRemove(state, level, pos, newState, isMoving);
}
}
@@ -0,0 +1,50 @@
package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
public final class BookStandBlockEntity extends BlockEntity {
private static final String BOOK_TAG = "Book";
private ItemStack book = ItemStack.EMPTY;
public BookStandBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.BOOK_STAND.get(), pos, state);
}
public ItemStack getBook() {
return book;
}
public void setBook(ItemStack book) {
this.book = book;
setChanged();
}
public ItemStack takeBook() {
ItemStack result = book;
book = ItemStack.EMPTY;
setChanged();
return result;
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
book = tag.contains(BOOK_TAG, CompoundTag.TAG_COMPOUND)
? ItemStack.parse(registries, tag.getCompound(BOOK_TAG)).orElse(ItemStack.EMPTY)
: ItemStack.EMPTY;
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
if (!book.isEmpty()) {
tag.put(BOOK_TAG, book.save(registries));
}
}
}
@@ -0,0 +1,49 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import net.dnorberg.rivencraft.block.BookStandBlock;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.minecraft.client.model.BookModel;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.EnchantTableRenderer;
import net.minecraft.world.level.block.state.BlockState;
public final class BookStandRenderer implements BlockEntityRenderer<BookStandBlockEntity> {
private final BookModel bookModel;
public BookStandRenderer(BlockEntityRendererProvider.Context context) {
bookModel = new BookModel(context.bakeLayer(ModelLayers.BOOK));
}
@Override
public void render(
BookStandBlockEntity blockEntity,
float partialTick,
PoseStack poseStack,
MultiBufferSource bufferSource,
int packedLight,
int packedOverlay
) {
BlockState state = blockEntity.getBlockState();
if (!state.getValue(BookStandBlock.HAS_BOOK)) {
return;
}
poseStack.pushPose();
poseStack.translate(0.5F, 1.0625F, 0.5F);
float rotation = state.getValue(BookStandBlock.FACING).getClockWise().toYRot();
poseStack.mulPose(Axis.YP.rotationDegrees(-rotation));
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5F));
poseStack.translate(0.0F, -0.125F, 0.0F);
bookModel.setupAnim(0.0F, 0.1F, 0.9F, 1.2F);
VertexConsumer consumer = EnchantTableRenderer.BOOK_LOCATION.buffer(bufferSource, RenderType::entitySolid);
bookModel.render(poseStack, consumer, packedLight, packedOverlay, -1);
poseStack.popPose();
}
}
@@ -0,0 +1,19 @@
package net.dnorberg.rivencraft.client;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
public final class ClientEvents {
private ClientEvents() {
}
@SubscribeEvent
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
}
}
@@ -0,0 +1,53 @@
package net.dnorberg.rivencraft.event;
import net.dnorberg.rivencraft.block.BookStandBlock;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
public final class BookStandConversionHandler {
private BookStandConversionHandler() {
}
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
ItemStack heldBook = event.getItemStack();
if (!heldBook.is(ModItems.LINKING_BOOK.get()) && !heldBook.is(ModItems.DESCRIPTIVE_BOOK.get())) {
return;
}
BlockState lectern = event.getLevel().getBlockState(event.getPos());
if (!lectern.is(Blocks.LECTERN) || lectern.getValue(LecternBlock.HAS_BOOK)) {
return;
}
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(event.getLevel().isClientSide));
if (event.getLevel().isClientSide) {
return;
}
BlockState bookStand = ModBlocks.BOOK_STAND.get().defaultBlockState()
.setValue(BookStandBlock.FACING, lectern.getValue(LecternBlock.FACING))
.setValue(BookStandBlock.HAS_BOOK, true);
event.getLevel().setBlock(event.getPos(), bookStand, 3);
if (event.getLevel().getBlockEntity(event.getPos()) instanceof BookStandBlockEntity stand) {
stand.setBook(heldBook.consumeAndReturn(1, event.getEntity()));
}
event.getLevel().playSound(
null,
event.getPos(),
SoundEvents.BOOK_PUT,
SoundSource.BLOCKS,
1.0F,
1.0F
);
}
}
@@ -20,8 +20,6 @@ import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
public final class LinkingBookItem extends Item {
private static final int LINK_COOLDOWN_TICKS = 40;
public LinkingBookItem(Properties properties) {
super(properties);
}
@@ -47,12 +45,21 @@ public final class LinkingBookItem extends Item {
return InteractionResultHolder.success(stack);
}
LinkingService.LinkResult result = LinkingService.travel(serverPlayer, target);
return useBoundBook(serverPlayer, stack);
}
public static InteractionResultHolder<ItemStack> useBoundBook(ServerPlayer player, ItemStack stack) {
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
if (target == null) {
player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.unbound"), true);
return InteractionResultHolder.fail(stack);
}
LinkingService.LinkResult result = LinkingService.travel(player, target);
if (result == LinkingService.LinkResult.SUCCESS) {
serverPlayer.getCooldowns().addCooldown(this, LINK_COOLDOWN_TICKS);
serverPlayer.level().playSound(
player.level().playSound(
null,
serverPlayer.blockPosition(),
player.blockPosition(),
SoundEvents.PORTAL_TRAVEL,
SoundSource.PLAYERS,
0.35F,
@@ -0,0 +1,26 @@
package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModBlockEntities {
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, Rivencraft.MOD_ID);
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<BookStandBlockEntity>> BOOK_STAND =
BLOCK_ENTITIES.register("book_stand", () -> BlockEntityType.Builder
.of(BookStandBlockEntity::new, ModBlocks.BOOK_STAND.get())
.build(null));
private ModBlockEntities() {
}
public static void register(IEventBus modEventBus) {
BLOCK_ENTITIES.register(modEventBus);
}
}
@@ -0,0 +1,26 @@
package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.BookStandBlock;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModBlocks {
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(Rivencraft.MOD_ID);
public static final DeferredBlock<BookStandBlock> BOOK_STAND = BLOCKS.registerBlock(
"book_stand",
BookStandBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.LECTERN)
);
private ModBlocks() {
}
public static void register(IEventBus modEventBus) {
BLOCKS.register(modEventBus);
}
}
@@ -0,0 +1,19 @@
{
"variants": {
"facing=east": {
"model": "minecraft:block/lectern",
"y": 90
},
"facing=north": {
"model": "minecraft:block/lectern"
},
"facing=south": {
"model": "minecraft:block/lectern",
"y": 180
},
"facing=west": {
"model": "minecraft:block/lectern",
"y": 270
}
}
}
@@ -6,6 +6,7 @@
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
"message.rivencraft.linking_book.unbound": "This Linking Book is unbound",
"message.rivencraft.linking_book.bound": "Linking Book bound",
"message.rivencraft.linking_book.linked": "Linked",
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
@@ -0,0 +1,16 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lectern"
}
],
"rolls": 1.0
}
],
"random_sequence": "rivencraft:blocks/book_stand"
}