1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-01-24 16:07:01 +00:00

Allow placing printouts in lecterns

- Add a new custom lectern block, that is used to hold the printed
   pages. We have to roll quite a lot of custom logic, so this is much
   cleaner than trying to mixin to the existing lectern code.

 - Add a new (entity) model for printed pages and books placed on a
   lectern. I did originally think about just rendering the item (or the
   in-hand/map version), but I think this is a bit more consistent with
   vanilla.

   However, we do still need to sync the item to the client (mostly to
   get the current page count!). There is a risk of chunkbanning here,
   but I think it's much harder than vanilla, due to the significantly
   reduced page limit.
This commit is contained in:
Jonathan Coates 2024-08-15 21:17:37 +01:00
parent 7e53c19d74
commit aa8078ddeb
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
20 changed files with 589 additions and 5 deletions

View File

@ -13,6 +13,7 @@ import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.CustomLecternRenderer;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
@ -73,6 +74,7 @@ public final class ClientRegistry {
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_ADVANCED.get(), TurtleBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.LECTERN.get(), CustomLecternRenderer::new);
}
/**

View File

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.render.CustomLecternRenderer;
import dan200.computercraft.shared.media.items.PrintoutItem;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.InventoryMenu;
import java.util.List;
/**
* A model for {@linkplain PrintoutItem printouts} placed on a lectern.
* <p>
* This provides two models, {@linkplain #renderPages(PoseStack, VertexConsumer, int, int, int) one for a variable
* number of pages}, and {@linkplain #renderBook(PoseStack, VertexConsumer, int, int) one for books}.
*
* @see CustomLecternRenderer
*/
public class LecternPrintoutModel {
public static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/printout");
public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE);
private static final int TEXTURE_WIDTH = 32;
private static final int TEXTURE_HEIGHT = 32;
private static final String PAGE_1 = "page_1";
private static final String PAGE_2 = "page_2";
private static final String PAGE_3 = "page_3";
private static final List<String> PAGES = List.of(PAGE_1, PAGE_2, PAGE_3);
private final ModelPart pagesRoot;
private final ModelPart bookRoot;
private final ModelPart[] pages;
public LecternPrintoutModel() {
pagesRoot = buildPages();
bookRoot = buildBook();
pages = PAGES.stream().map(pagesRoot::getChild).toArray(ModelPart[]::new);
}
private static ModelPart buildPages() {
var mesh = new MeshDefinition();
var parts = mesh.getRoot();
parts.addOrReplaceChild(
PAGE_1,
CubeListBuilder.create().texOffs(0, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f),
PartPose.ZERO
);
parts.addOrReplaceChild(
PAGE_2,
CubeListBuilder.create().texOffs(12, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f),
PartPose.offsetAndRotation(-0.125f, 0, 1.5f, (float) Math.PI * (1f / 16), 0, 0)
);
parts.addOrReplaceChild(
PAGE_3,
CubeListBuilder.create().texOffs(12, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f),
PartPose.offsetAndRotation(-0.25f, 0, -1.5f, (float) -Math.PI * (2f / 16), 0, 0)
);
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
}
private static ModelPart buildBook() {
var mesh = new MeshDefinition();
var parts = mesh.getRoot();
parts.addOrReplaceChild(
"spine",
CubeListBuilder.create().texOffs(12, 15).addBox(-0.005f, -5.0f, -0.5f, 0, 10, 1.0f),
PartPose.ZERO
);
var angle = (float) Math.toRadians(5);
parts.addOrReplaceChild(
"left",
CubeListBuilder.create()
.texOffs(0, 10).addBox(0, -5.0f, -6.0f, 0, 10, 6.0f)
.texOffs(0, 0).addBox(0.005f, -4.0f, -5.0f, 1.0f, 8.0f, 5.0f),
PartPose.offsetAndRotation(-0.005f, 0, -0.5f, 0, -angle, 0)
);
parts.addOrReplaceChild(
"right",
CubeListBuilder.create()
.texOffs(14, 10).addBox(0, -5.0f, 0, 0, 10, 6.0f)
.texOffs(0, 0).addBox(0.005f, -4.0f, 0, 1.0f, 8.0f, 5.0f),
PartPose.offsetAndRotation(-0.005f, 0, 0.5f, 0, angle, 0)
);
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
}
public void renderBook(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay) {
bookRoot.render(poseStack, buffer, packedLight, packedOverlay, 1, 1, 1, 1);
}
public void renderPages(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay, int pageCount) {
if (pageCount > pages.length) pageCount = pages.length;
var i = 0;
for (; i < pageCount; i++) pages[i].visible = true;
for (; i < pages.length; i++) pages[i].visible = false;
pagesRoot.render(poseStack, buffer, packedLight, packedOverlay, 1, 1, 1, 1);
}
}

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
import dan200.computercraft.shared.media.items.PrintoutItem;
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.LecternRenderer;
import net.minecraft.world.level.block.LecternBlock;
/**
* A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}.
* <p>
* This largely follows {@link LecternRenderer}, but with support for multiple types of item.
*/
public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternBlockEntity> {
private final LecternPrintoutModel printoutModel;
public CustomLecternRenderer(BlockEntityRendererProvider.Context context) {
printoutModel = new LecternPrintoutModel();
}
@Override
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) {
poseStack.pushPose();
poseStack.translate(0.5f, 1.0625f, 0.5f);
poseStack.mulPose(Axis.YP.rotationDegrees(-lectern.getBlockState().getValue(LecternBlock.FACING).getClockWise().toYRot()));
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5f));
poseStack.translate(0, -0.125f, 0);
var item = lectern.getItem();
if (item.getItem() instanceof PrintoutItem printout) {
var vertexConsumer = LecternPrintoutModel.MATERIAL.buffer(buffer, RenderType::entitySolid);
if (printout.getType() == PrintoutItem.Type.BOOK) {
printoutModel.renderBook(poseStack, vertexConsumer, packedLight, packedOverlay);
} else {
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutItem.getPageCount(item));
}
}
poseStack.popPose();
}
}

View File

@ -5,6 +5,7 @@
package dan200.computercraft.data.client;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.data.DataProviders;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
@ -30,7 +31,8 @@ public final class ClientDataProviders {
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(new ResourceLocation("blocks"), List.of(
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty())
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()),
new SingleFile(LecternPrintoutModel.TEXTURE, Optional.empty())
));
out.accept(GuiSprites.SPRITE_SHEET, Stream.of(
// Buttons

View File

@ -0,0 +1,8 @@
{
"variants": {
"facing=east": {"model": "minecraft:block/lectern", "y": 90},
"facing=north": {"model": "minecraft:block/lectern", "y": 0},
"facing=south": {"model": "minecraft:block/lectern", "y": 180},
"facing=west": {"model": "minecraft:block/lectern", "y": 270}
}
}

View File

@ -1,6 +1,7 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"},
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"}
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"},
{"type": "minecraft:single", "resource": "computercraft:entity/printout"}
]
}

View File

@ -0,0 +1,12 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [{"condition": "minecraft:survives_explosion"}],
"entries": [{"type": "minecraft:item", "name": "minecraft:lectern"}],
"rolls": 1.0
}
],
"random_sequence": "computercraft:blocks/lectern"
}

View File

@ -23,6 +23,7 @@ import net.minecraft.data.models.BlockModelGenerators;
import net.minecraft.data.models.blockstates.*;
import net.minecraft.data.models.model.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.Property;
@ -100,6 +101,11 @@ class BlockModelProvider {
registerTurtleUpgrade(generators, "block/turtle_speaker", "block/turtle_speaker_face");
registerTurtleModem(generators, "block/turtle_modem_normal", "block/wireless_modem_normal_face");
registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(
ModRegistry.Blocks.LECTERN.get(),
Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(Blocks.LECTERN))
).with(createHorizontalFacingDispatch()));
}
private static void registerDiskDrive(BlockModelGenerators generators) {

View File

@ -284,7 +284,9 @@ public final class LanguageProvider implements DataProvider {
return Stream.of(
RegistryWrappers.BLOCKS.stream()
.filter(x -> RegistryWrappers.BLOCKS.getKey(x).getNamespace().equals(ComputerCraftAPI.MOD_ID))
.map(Block::getDescriptionId),
.map(Block::getDescriptionId)
// Exclude blocks that just reuse vanilla translations, such as the lectern.
.filter(x -> !x.startsWith("block.minecraft.")),
RegistryWrappers.ITEMS.stream()
.filter(x -> RegistryWrappers.ITEMS.getKey(x).getNamespace().equals(ComputerCraftAPI.MOD_ID))
.map(Item::getDescriptionId),

View File

@ -15,6 +15,7 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
import net.minecraft.advancements.critereon.StatePropertiesPredicate;
import net.minecraft.data.loot.LootTableProvider.SubProviderEntry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.storage.loot.LootPool;
import net.minecraft.world.level.storage.loot.LootTable;
@ -57,6 +58,8 @@ class LootTableProvider {
computerDrop(add, ModRegistry.Blocks.TURTLE_NORMAL);
computerDrop(add, ModRegistry.Blocks.TURTLE_ADVANCED);
blockDrop(add, ModRegistry.Blocks.LECTERN, LootItem.lootTableItem(Items.LECTERN), ExplosionCondition.survivesExplosion());
add.accept(ModRegistry.Blocks.CABLE.get().getLootTable(), LootTable
.lootTable()
.withPool(LootPool.lootPool()

View File

@ -102,8 +102,14 @@ class TagProvider {
ModRegistry.Items.MONITOR_ADVANCED.get()
);
// Allow printed books to be placed in bookshelves.
tags.tag(ItemTags.BOOKSHELF_BOOKS).add(ModRegistry.Items.PRINTED_BOOK.get());
// Allow any printout to be placed on lecterns. See also PrintoutItem and CustomLecternBlock.
tags.tag(ItemTags.LECTERN_BOOKS).add(
ModRegistry.Items.PRINTED_PAGE.get(), ModRegistry.Items.PRINTED_PAGES.get(), ModRegistry.Items.PRINTED_BOOK.get()
);
tags.tag(ComputerCraftTags.Items.TURTLE_CAN_PLACE)
.add(Items.GLASS_BOTTLE)
.addTag(ItemTags.BOATS);

View File

@ -40,6 +40,8 @@ import dan200.computercraft.shared.data.PlayerCreativeLootCondition;
import dan200.computercraft.shared.details.BlockDetails;
import dan200.computercraft.shared.details.ItemDetails;
import dan200.computercraft.shared.integration.PermissionRegistry;
import dan200.computercraft.shared.lectern.CustomLecternBlock;
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
import dan200.computercraft.shared.media.PrintoutMenu;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.PrintoutItem;
@ -100,10 +102,12 @@ import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
@ -171,6 +175,10 @@ public final class ModRegistry {
public static final RegistryEntry<WiredModemFullBlock> WIRED_MODEM_FULL = REGISTRY.register("wired_modem_full",
() -> new WiredModemFullBlock(modemProperties().mapColor(MapColor.STONE)));
public static final RegistryEntry<CableBlock> CABLE = REGISTRY.register("cable", () -> new CableBlock(modemProperties().mapColor(MapColor.STONE)));
public static final RegistryEntry<CustomLecternBlock> LECTERN = REGISTRY.register("lectern", () -> new CustomLecternBlock(
BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).strength(2.5F).sound(SoundType.WOOD).ignitedByLava()
));
}
public static class BlockEntities {
@ -212,6 +220,8 @@ public final class ModRegistry {
ofBlock(Blocks.WIRELESS_MODEM_NORMAL, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_NORMAL.get(), p, s, false));
public static final RegistryEntry<BlockEntityType<WirelessModemBlockEntity>> WIRELESS_MODEM_ADVANCED =
ofBlock(Blocks.WIRELESS_MODEM_ADVANCED, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_ADVANCED.get(), p, s, true));
public static final RegistryEntry<BlockEntityType<CustomLecternBlockEntity>> LECTERN = ofBlock(Blocks.LECTERN, CustomLecternBlockEntity::new);
}
public static final class Items {

View File

@ -4,16 +4,17 @@
package dan200.computercraft.shared.container;
import net.minecraft.core.NonNullList;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.item.ItemStack;
import java.util.List;
/**
* A basic implementation of {@link Container} which operates on a {@linkplain #getContents() list of stacks}.
*/
public interface BasicContainer extends Container {
NonNullList<ItemStack> getContents();
List<ItemStack> getContents();
@Override
default int getContainerSize() {

View File

@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.lectern;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.items.PrintoutItem;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.stats.Stats;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
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.item.Items;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.BlockGetter;
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.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
/**
* Extends {@link LecternBlock} with support for {@linkplain PrintoutItem printouts}.
* <p>
* Unlike the vanilla lectern, this block is never empty. If the book is removed from the lectern, it converts back to
* its vanilla version (see {@link #clearLectern(Level, BlockPos, BlockState)}).
*
* @see PrintoutItem#useOn(UseOnContext) Placing books into a lectern.
*/
public class CustomLecternBlock extends LecternBlock {
public CustomLecternBlock(Properties properties) {
super(properties);
registerDefaultState(defaultBlockState().setValue(HAS_BOOK, true));
}
/**
* Replace a vanilla lectern with a custom one.
*
* @param level The current level.
* @param pos The position of the lectern.
* @param blockState The current state of the lectern.
* @param item The item to place in the custom lectern.
*/
public static void replaceLectern(Level level, BlockPos pos, BlockState blockState, ItemStack item) {
level.setBlockAndUpdate(pos, ModRegistry.Blocks.LECTERN.get().defaultBlockState()
.setValue(HAS_BOOK, true)
.setValue(FACING, blockState.getValue(FACING))
.setValue(POWERED, blockState.getValue(POWERED)));
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity be) be.setItem(item.split(1));
}
/**
* Remove a custom lectern and replace it with an empty vanilla one.
*
* @param level The current level.
* @param pos The position of the lectern.
* @param blockState The current state of the lectern.
*/
static void clearLectern(Level level, BlockPos pos, BlockState blockState) {
level.setBlockAndUpdate(pos, Blocks.LECTERN.defaultBlockState()
.setValue(HAS_BOOK, false)
.setValue(FACING, blockState.getValue(FACING))
.setValue(POWERED, blockState.getValue(POWERED)));
}
@Override
@Deprecated
public ItemStack getCloneItemStack(BlockGetter level, BlockPos pos, BlockState state) {
return new ItemStack(Items.LECTERN);
}
@Override
public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
// If we've no lectern, remove it.
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern && lectern.getItem().isEmpty()) {
clearLectern(level, pos, state);
return;
}
super.tick(state, level, pos, random);
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
if (state.is(newState.getBlock())) return;
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern) {
dropItem(level, pos, state, lectern.getItem().copy());
}
super.onRemove(state, level, pos, newState, isMoving);
}
private static void dropItem(Level level, BlockPos pos, BlockState state, ItemStack stack) {
if (stack.isEmpty()) return;
var direction = state.getValue(FACING);
var dx = 0.25 * direction.getStepX();
var dz = 0.25 * direction.getStepZ();
var entity = new ItemEntity(level, pos.getX() + 0.5 + dx, pos.getY() + 1, pos.getZ() + 0.5 + dz, stack);
entity.setDefaultPickUpDelay();
level.addFreshEntity(entity);
}
@Override
public String getDescriptionId() {
return Blocks.LECTERN.getDescriptionId();
}
@Override
public CustomLecternBlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new CustomLecternBlockEntity(pos, state);
}
@Override
public int getAnalogOutputSignal(BlockState blockState, Level level, BlockPos pos) {
return level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern ? lectern.getRedstoneSignal() : 0;
}
@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (!level.isClientSide && level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern) {
if (player.isSecondaryUseActive()) {
// When shift+clicked with an empty hand, drop the item and replace with the normal lectern.
clearLectern(level, pos, state);
} else {
// Otherwise open the screen.
player.openMenu(lectern);
}
player.awardStat(Stats.INTERACT_WITH_LECTERN);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
}

View File

@ -0,0 +1,193 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.lectern;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.container.BasicContainer;
import dan200.computercraft.shared.container.SingleContainerData;
import dan200.computercraft.shared.media.PrintoutMenu;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.util.Mth;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
import java.util.AbstractList;
import java.util.List;
/**
* The block entity for our {@link CustomLecternBlock}.
*
* @see LecternBlockEntity
*/
public final class CustomLecternBlockEntity extends BlockEntity implements MenuProvider {
private static final String NBT_ITEM = "Item";
private static final String NBT_PAGE = "Page";
private ItemStack item = ItemStack.EMPTY;
private int page, pageCount;
public CustomLecternBlockEntity(BlockPos pos, BlockState blockState) {
super(ModRegistry.BlockEntities.LECTERN.get(), pos, blockState);
}
public ItemStack getItem() {
return item;
}
void setItem(ItemStack item) {
this.item = item;
itemChanged();
BlockEntityHelpers.updateBlock(this);
}
int getRedstoneSignal() {
if (item.getItem() instanceof PrintoutItem) {
var progress = pageCount > 1 ? (float) page / (pageCount - 1) : 1F;
return Mth.floor(progress * 14f) + 1;
}
return 15;
}
/**
* Called after the item has changed. This sets up the state for the new item.
*/
private void itemChanged() {
if (item.getItem() instanceof PrintoutItem) {
pageCount = PrintoutItem.getPageCount(item);
page = Mth.clamp(page, 0, pageCount - 1);
} else {
pageCount = page = 0;
}
}
/**
* Set the current page, emitting a redstone pulse if needed.
*
* @param page The new page.
*/
private void setPage(int page) {
if (this.page == page) return;
this.page = page;
setChanged();
if (getLevel() != null) LecternBlock.signalPageChange(getLevel(), getBlockPos(), getBlockState());
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
item = tag.contains(NBT_ITEM, Tag.TAG_COMPOUND) ? ItemStack.of(tag.getCompound(NBT_ITEM)) : ItemStack.EMPTY;
page = tag.getInt(NBT_PAGE);
itemChanged();
}
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
if (!item.isEmpty()) tag.put(NBT_ITEM, item.save(new CompoundTag()));
if (item.getItem() instanceof PrintoutItem) tag.putInt(NBT_PAGE, page);
}
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public CompoundTag getUpdateTag() {
var tag = super.getUpdateTag();
tag.put(NBT_ITEM, item.save(new CompoundTag()));
return tag;
}
@Nullable
@Override
public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
var item = getItem();
if (item.getItem() instanceof PrintoutItem) {
return new PrintoutMenu(
containerId, new LecternContainer(), 0,
p -> Container.stillValidBlockEntity(this, player, Container.DEFAULT_DISTANCE_LIMIT),
new PrintoutContainerData()
);
}
return null;
}
@Override
public Component getDisplayName() {
return getItem().getDisplayName();
}
/**
* A read-only container storing the lectern's contents.
*/
private final class LecternContainer implements BasicContainer {
private final List<ItemStack> itemView = new AbstractList<>() {
@Override
public ItemStack get(int index) {
if (index != 0) throw new IndexOutOfBoundsException("Inventory only has one slot");
return item;
}
@Override
public int size() {
return 1;
}
};
@Override
public List<ItemStack> getContents() {
return itemView;
}
@Override
public void setChanged() {
// Should never happen, so a no-op.
}
@Override
public boolean stillValid(Player player) {
return !isRemoved();
}
}
/**
* {@link ContainerData} for a {@link PrintoutMenu}. This provides a read/write view of the current page.
*/
private final class PrintoutContainerData implements SingleContainerData {
@Override
public int get() {
return page;
}
@Override
public void set(int index, int value) {
if (index == 0) setPage(value);
}
}
}

View File

@ -6,6 +6,7 @@ package dan200.computercraft.shared.media.items;
import com.google.common.base.Strings;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.lectern.CustomLecternBlock;
import dan200.computercraft.shared.media.PrintoutMenu;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
@ -16,7 +17,10 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LecternBlock;
import javax.annotation.Nullable;
import java.util.List;
@ -50,6 +54,22 @@ public class PrintoutItem extends Item {
if (title != null && !title.isEmpty()) list.add(Component.literal(title));
}
@Override
public InteractionResult useOn(UseOnContext context) {
var level = context.getLevel();
var blockPos = context.getClickedPos();
var blockState = level.getBlockState(blockPos);
if (blockState.is(Blocks.LECTERN) && !blockState.getValue(LecternBlock.HAS_BOOK)) {
// If we have an empty lectern, place our book into it.
if (!level.isClientSide) {
CustomLecternBlock.replaceLectern(level, blockPos, blockState, context.getItemInHand());
}
return InteractionResult.sidedSuccess(level.isClientSide);
} else {
return InteractionResult.PASS;
}
}
@Override
public InteractionResultHolder<ItemStack> use(Level world, Player player, InteractionHand hand) {
var stack = player.getItemInHand(hand);

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

View File

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0

View File

@ -0,0 +1,4 @@
{
"replace": false,
"values": ["computercraft:printed_page", "computercraft:printed_pages", "computercraft:printed_book"]
}

View File

@ -0,0 +1 @@
{"values": ["computercraft:printed_page", "computercraft:printed_pages", "computercraft:printed_book"]}