diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java index 405f810ab..b1560568c 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -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); } /** diff --git a/projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java b/projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java new file mode 100644 index 000000000..5e9930094 --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/model/LecternPrintoutModel.java @@ -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. + *

+ * 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 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); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java new file mode 100644 index 000000000..1743d2bfd --- /dev/null +++ b/projects/common/src/client/java/dan200/computercraft/client/render/CustomLecternRenderer.java @@ -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}. + *

+ * This largely follows {@link LecternRenderer}, but with support for multiple types of item. + */ +public class CustomLecternRenderer implements BlockEntityRenderer { + 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(); + } +} diff --git a/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java b/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java index 76891e250..b318826d0 100644 --- a/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java +++ b/projects/common/src/client/java/dan200/computercraft/data/client/ClientDataProviders.java @@ -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 diff --git a/projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json b/projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json new file mode 100644 index 000000000..c760753f3 --- /dev/null +++ b/projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json @@ -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} + } +} diff --git a/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json b/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json index d9d236296..eb21f8f98 100644 --- a/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json +++ b/projects/common/src/generated/resources/assets/minecraft/atlases/blocks.json @@ -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"} ] } diff --git a/projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json b/projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json new file mode 100644 index 000000000..b5876ca42 --- /dev/null +++ b/projects/common/src/generated/resources/data/computercraft/loot_tables/blocks/lectern.json @@ -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" +} diff --git a/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java b/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java index c0c9f3491..274557ad2 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/BlockModelProvider.java @@ -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) { diff --git a/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java b/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java index 585f77b1e..2dc57e405 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/LanguageProvider.java @@ -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), diff --git a/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java b/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java index 33c9d239b..5f0f5b5ac 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/LootTableProvider.java @@ -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() diff --git a/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java b/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java index 833287af6..795ecca15 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/TagProvider.java @@ -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); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index 9d16b6ee3..5cb731e14 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -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 WIRED_MODEM_FULL = REGISTRY.register("wired_modem_full", () -> new WiredModemFullBlock(modemProperties().mapColor(MapColor.STONE))); public static final RegistryEntry CABLE = REGISTRY.register("cable", () -> new CableBlock(modemProperties().mapColor(MapColor.STONE))); + + public static final RegistryEntry 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> WIRELESS_MODEM_ADVANCED = ofBlock(Blocks.WIRELESS_MODEM_ADVANCED, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_ADVANCED.get(), p, s, true)); + + public static final RegistryEntry> LECTERN = ofBlock(Blocks.LECTERN, CustomLecternBlockEntity::new); } public static final class Items { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java b/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java index 6680725da..7dd51b29f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/container/BasicContainer.java @@ -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 getContents(); + List getContents(); @Override default int getContainerSize() { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java new file mode 100644 index 000000000..f79417f2f --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlock.java @@ -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}. + *

+ * 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); + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java new file mode 100644 index 000000000..ee124ed15 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/lectern/CustomLecternBlockEntity.java @@ -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 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 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 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); + } + } +} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java b/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java index 31d2a6e86..6aed3babf 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/items/PrintoutItem.java @@ -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 use(Level world, Player player, InteractionHand hand) { var stack = player.getItemInHand(hand); diff --git a/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png b/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png new file mode 100644 index 000000000..204b0483f Binary files /dev/null and b/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png differ diff --git a/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png.license b/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png.license new file mode 100644 index 000000000..1377dfd3b --- /dev/null +++ b/projects/common/src/main/resources/assets/computercraft/textures/entity/printout.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers + +SPDX-License-Identifier: MPL-2.0 diff --git a/projects/fabric/src/generated/resources/data/minecraft/tags/items/lectern_books.json b/projects/fabric/src/generated/resources/data/minecraft/tags/items/lectern_books.json new file mode 100644 index 000000000..818fe0ecf --- /dev/null +++ b/projects/fabric/src/generated/resources/data/minecraft/tags/items/lectern_books.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["computercraft:printed_page", "computercraft:printed_pages", "computercraft:printed_book"] +} diff --git a/projects/forge/src/generated/resources/data/minecraft/tags/items/lectern_books.json b/projects/forge/src/generated/resources/data/minecraft/tags/items/lectern_books.json new file mode 100644 index 000000000..80ec25824 --- /dev/null +++ b/projects/forge/src/generated/resources/data/minecraft/tags/items/lectern_books.json @@ -0,0 +1 @@ +{"values": ["computercraft:printed_page", "computercraft:printed_pages", "computercraft:printed_book"]}