diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java index 8ed3b8f64..626ee07d0 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/PrintoutScreen.java @@ -6,15 +6,22 @@ package dan200.computercraft.client.gui; import com.mojang.blaze3d.vertex.Tesselator; import dan200.computercraft.core.terminal.TextBuffer; -import dan200.computercraft.shared.common.HeldItemMenu; +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.media.PrintoutMenu; import dan200.computercraft.shared.media.items.PrintoutItem; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.item.ItemStack; import org.lwjgl.glfw.GLFW; +import java.util.Arrays; +import java.util.Objects; + import static dan200.computercraft.client.render.PrintoutRenderer.*; import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP; @@ -23,40 +30,75 @@ import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMA * * @see dan200.computercraft.client.render.PrintoutRenderer */ -public class PrintoutScreen extends AbstractContainerScreen { - private final boolean book; - private final int pages; - private final TextBuffer[] text; - private final TextBuffer[] colours; - private int page; +public final class PrintoutScreen extends AbstractContainerScreen implements ContainerListener { + private PrintoutInfo printout = PrintoutInfo.DEFAULT; + private int page = 0; - public PrintoutScreen(HeldItemMenu container, Inventory player, Component title) { + public PrintoutScreen(PrintoutMenu container, Inventory player, Component title) { super(container, player, title); - imageHeight = Y_SIZE; + } - var text = PrintoutItem.getText(container.getStack()); - this.text = new TextBuffer[text.length]; - for (var i = 0; i < this.text.length; i++) this.text[i] = new TextBuffer(text[i]); + private void setPrintout(ItemStack stack) { + var text = PrintoutItem.getText(stack); + var textBuffers = new TextBuffer[text.length]; + for (var i = 0; i < textBuffers.length; i++) textBuffers[i] = new TextBuffer(text[i]); - var colours = PrintoutItem.getColours(container.getStack()); - this.colours = new TextBuffer[colours.length]; - for (var i = 0; i < this.colours.length; i++) this.colours[i] = new TextBuffer(colours[i]); + var colours = PrintoutItem.getColours(stack); + var colourBuffers = new TextBuffer[colours.length]; + for (var i = 0; i < colours.length; i++) colourBuffers[i] = new TextBuffer(colours[i]); - page = 0; - pages = Math.max(this.text.length / PrintoutItem.LINES_PER_PAGE, 1); - book = ((PrintoutItem) container.getStack().getItem()).getType() == PrintoutItem.Type.BOOK; + var pages = Math.max(text.length / PrintoutItem.LINES_PER_PAGE, 1); + var book = stack.is(ModRegistry.Items.PRINTED_BOOK.get()); + + printout = new PrintoutInfo(pages, book, textBuffers, colourBuffers); + } + + @Override + protected void init() { + super.init(); + menu.addSlotListener(this); + } + + @Override + public void removed() { + menu.removeSlotListener(this); + } + + @Override + public void slotChanged(AbstractContainerMenu menu, int slot, ItemStack stack) { + if (slot == 0) setPrintout(stack); + } + + @Override + public void dataChanged(AbstractContainerMenu menu, int slot, int data) { + if (slot == PrintoutMenu.DATA_CURRENT_PAGE) page = data; + } + + private void setPage(int page) { + this.page = page; + + var gameMode = Objects.requireNonNull(Objects.requireNonNull(minecraft).gameMode); + gameMode.handleInventoryButtonClick(menu.containerId, PrintoutMenu.PAGE_BUTTON_OFFSET + page); + } + + private void previousPage() { + if (page > 0) setPage(page - 1); + } + + private void nextPage() { + if (page < printout.pages() - 1) setPage(page + 1); } @Override public boolean keyPressed(int key, int scancode, int modifiers) { if (key == GLFW.GLFW_KEY_RIGHT) { - if (page < pages - 1) page++; + nextPage(); return true; } if (key == GLFW.GLFW_KEY_LEFT) { - if (page > 0) page--; + previousPage(); return true; } @@ -68,13 +110,13 @@ public class PrintoutScreen extends AbstractContainerScreen { if (super.mouseScrolled(x, y, delta)) return true; if (delta < 0) { // Scroll up goes to the next page - if (page < pages - 1) page++; + nextPage(); return true; } if (delta > 0) { // Scroll down goes to the previous page - if (page > 0) page--; + previousPage(); return true; } @@ -85,8 +127,9 @@ public class PrintoutScreen extends AbstractContainerScreen { protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) { // Draw the printout var renderer = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); - drawBorder(graphics.pose(), renderer, leftPos, topPos, 0, page, pages, book, FULL_BRIGHT_LIGHTMAP); - drawText(graphics.pose(), renderer, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutItem.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, text, colours); + + drawBorder(graphics.pose(), renderer, leftPos, topPos, 0, page, printout.pages(), printout.book(), FULL_BRIGHT_LIGHTMAP); + drawText(graphics.pose(), renderer, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutItem.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, printout.text(), printout.colour()); renderer.endBatch(); } @@ -105,4 +148,18 @@ public class PrintoutScreen extends AbstractContainerScreen { protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { // Skip rendering labels. } + + record PrintoutInfo(int pages, boolean book, TextBuffer[] text, TextBuffer[] colour) { + public static final PrintoutInfo DEFAULT; + + static { + var textLines = new TextBuffer[PrintoutItem.LINES_PER_PAGE]; + Arrays.fill(textLines, new TextBuffer(" ".repeat(PrintoutItem.LINE_MAX_LENGTH))); + + var colourLines = new TextBuffer[PrintoutItem.LINES_PER_PAGE]; + Arrays.fill(colourLines, new TextBuffer("f".repeat(PrintoutItem.LINE_MAX_LENGTH))); + + DEFAULT = new PrintoutInfo(1, false, textLines, colourLines); + } + } } 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 12446bb61..9d16b6ee3 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -23,7 +23,6 @@ import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType; import dan200.computercraft.shared.common.ClearColourRecipe; import dan200.computercraft.shared.common.ColourableRecipe; import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; -import dan200.computercraft.shared.common.HeldItemMenu; import dan200.computercraft.shared.computer.apis.CommandAPI; import dan200.computercraft.shared.computer.blocks.CommandComputerBlock; import dan200.computercraft.shared.computer.blocks.ComputerBlock; @@ -41,6 +40,7 @@ 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.media.PrintoutMenu; import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.media.items.RecordMedia; @@ -49,7 +49,6 @@ import dan200.computercraft.shared.media.recipes.DiskRecipe; import dan200.computercraft.shared.media.recipes.PrintoutRecipe; import dan200.computercraft.shared.network.container.ComputerContainerData; import dan200.computercraft.shared.network.container.ContainerData; -import dan200.computercraft.shared.network.container.HeldItemContainerData; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu; @@ -309,11 +308,8 @@ public final class ModRegistry { public static final RegistryEntry> PRINTER = REGISTRY.register("printer", () -> new MenuType<>(PrinterMenu::new, FeatureFlags.VANILLA_SET)); - public static final RegistryEntry> PRINTOUT = REGISTRY.register("printout", - () -> ContainerData.toType( - HeldItemContainerData::new, - (id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.getHand()) - )); + public static final RegistryEntry> PRINTOUT = REGISTRY.register("printout", + () -> new MenuType<>((i, c) -> PrintoutMenu.createRemote(i), FeatureFlags.VANILLA_SET)); } static class ArgumentTypes { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java deleted file mode 100644 index 7a4c5895d..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/common/HeldItemMenu.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. -// -// SPDX-License-Identifier: LicenseRef-CCPL - -package dan200.computercraft.shared.common; - -import net.minecraft.network.chat.Component; -import net.minecraft.world.InteractionHand; -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.MenuType; -import net.minecraft.world.item.ItemStack; - -import javax.annotation.Nullable; - -public class HeldItemMenu extends AbstractContainerMenu { - private final ItemStack stack; - private final InteractionHand hand; - - public HeldItemMenu(MenuType type, int id, Player player, InteractionHand hand) { - super(type, id); - - this.hand = hand; - stack = player.getItemInHand(hand).copy(); - } - - public ItemStack getStack() { - return stack; - } - - @Override - public ItemStack quickMoveStack(Player player, int slot) { - return ItemStack.EMPTY; - } - - @Override - public boolean stillValid(Player player) { - if (!player.isAlive()) return false; - - var stack = player.getItemInHand(hand); - return stack == this.stack || !stack.isEmpty() && !this.stack.isEmpty() && stack.getItem() == this.stack.getItem(); - } - - public static class Factory implements MenuProvider { - private final MenuType type; - private final Component name; - private final InteractionHand hand; - - public Factory(MenuType type, ItemStack stack, InteractionHand hand) { - this.type = type; - name = stack.getHoverName(); - this.hand = hand; - } - - @Override - public Component getDisplayName() { - return name; - } - - @Nullable - @Override - public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) { - return new HeldItemMenu(type, id, player, hand); - } - } -} diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java b/projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java new file mode 100644 index 000000000..ffa7097bb --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/PrintoutMenu.java @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.media; + +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.container.InvisibleSlot; +import dan200.computercraft.shared.media.items.PrintoutItem; +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; + +import java.util.function.Predicate; + +/** + * The menus for {@linkplain PrintoutItem printouts}. + *

+ * This is a somewhat similar design to {@link LecternMenu}, which is used to read written books. + *

+ * This holds a single slot (containing the printout), and a single data slot ({@linkplain #DATA_CURRENT_PAGE holding + * the current page}). The page is set by the client by sending a {@linkplain #clickMenuButton(Player, int) button + * press} with an index of {@link #PAGE_BUTTON_OFFSET} plus the current page. + *

+ * The client-side screen uses {@linkplain ContainerListener container listeners} to subscribe to item and page changes. + * However, listeners aren't fired on the client, so we copy {@link LecternMenu}'s hack and call + * {@link #broadcastChanges()} whenever an item or data value are changed. + */ +public class PrintoutMenu extends AbstractContainerMenu { + public static final int DATA_CURRENT_PAGE = 0; + private static final int DATA_SIZE = 1; + + public static final int PAGE_BUTTON_OFFSET = 100; + + private final Predicate valid; + private final ContainerData currentPage; + + public PrintoutMenu( + int containerId, Container container, int slotIdx, Predicate valid, ContainerData currentPage + ) { + super(ModRegistry.Menus.PRINTOUT.get(), containerId); + this.valid = valid; + this.currentPage = currentPage; + + addSlot(new InvisibleSlot(container, slotIdx) { + @Override + public void setChanged() { + super.setChanged(); + slotsChanged(container); // Trigger listeners on the client. + } + }); + addDataSlots(currentPage); + } + + /** + * Create {@link PrintoutMenu} for use a remote (client). + * + * @param containerId The current container id. + * @return The constructed container. + */ + public static PrintoutMenu createRemote(int containerId) { + return new PrintoutMenu(containerId, new SimpleContainer(1), 0, p -> true, new SimpleContainerData(DATA_SIZE)); + } + + /** + * Create a {@link PrintoutMenu} for the printout in the current player's hand. + * + * @param containerId The current container id. + * @param player The player to open the container. + * @param hand The hand containing the item. + * @return The constructed container. + */ + public static PrintoutMenu createInHand(int containerId, Player player, InteractionHand hand) { + var currentStack = player.getItemInHand(hand); + var currentItem = currentStack.getItem(); + + var slot = switch (hand) { + case MAIN_HAND -> player.getInventory().selected; + case OFF_HAND -> Inventory.SLOT_OFFHAND; + }; + return new PrintoutMenu( + containerId, player.getInventory(), slot, + p -> player.getItemInHand(hand).getItem() == currentItem, new SimpleContainerData(DATA_SIZE) + ); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + return valid.test(player); + } + + @Override + public boolean clickMenuButton(Player player, int id) { + if (id >= PAGE_BUTTON_OFFSET) { + var page = Mth.clamp(id - PAGE_BUTTON_OFFSET, 0, PrintoutItem.getPageCount(getPrintout()) - 1); + setData(DATA_CURRENT_PAGE, page); + return true; + } + + return super.clickMenuButton(player, id); + } + + /** + * Get the current printout. + * + * @return The current printout. + */ + public ItemStack getPrintout() { + return getSlot(0).getItem(); + } + + /** + * Get the current page. + * + * @return The current page. + */ + public int getPage() { + return currentPage.get(DATA_CURRENT_PAGE); + } + + @Override + public void setData(int id, int data) { + super.setData(id, data); + broadcastChanges(); // Trigger listeners on the client. + } +} 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 c99f2b756..31d2a6e86 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 @@ -4,13 +4,14 @@ package dan200.computercraft.shared.media.items; +import com.google.common.base.Strings; import dan200.computercraft.shared.ModRegistry; -import dan200.computercraft.shared.common.HeldItemMenu; -import dan200.computercraft.shared.network.container.HeldItemContainerData; +import dan200.computercraft.shared.media.PrintoutMenu; import net.minecraft.network.chat.Component; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.SimpleMenuProvider; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -51,11 +52,13 @@ public class PrintoutItem extends Item { @Override public InteractionResultHolder use(Level world, Player player, InteractionHand hand) { + var stack = player.getItemInHand(hand); if (!world.isClientSide) { - new HeldItemContainerData(hand) - .open(player, new HeldItemMenu.Factory(ModRegistry.Menus.PRINTOUT.get(), player.getItemInHand(hand), hand)); + var title = getTitle(stack); + var displayTitle = Strings.isNullOrEmpty(title) ? stack.getDisplayName() : Component.literal(title); + player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> PrintoutMenu.createInHand(id, p, hand), displayTitle)); } - return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), player.getItemInHand(hand)); + return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack); } private ItemStack createFromTitleAndText(@Nullable String title, @Nullable String[] text, @Nullable String[] colours) { diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java b/projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java deleted file mode 100644 index 8bbb6522b..000000000 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/container/HeldItemContainerData.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared.network.container; - -import dan200.computercraft.shared.common.HeldItemMenu; -import dan200.computercraft.shared.media.items.PrintoutItem; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.world.InteractionHand; - -/** - * Opens a printout GUI based on the currently held item. - * - * @see HeldItemMenu - * @see PrintoutItem - */ -public class HeldItemContainerData implements ContainerData { - private final InteractionHand hand; - - public HeldItemContainerData(InteractionHand hand) { - this.hand = hand; - } - - public HeldItemContainerData(FriendlyByteBuf buffer) { - hand = buffer.readEnum(InteractionHand.class); - } - - @Override - public void toBytes(FriendlyByteBuf buf) { - buf.writeEnum(hand); - } - - public InteractionHand getHand() { - return hand; - } -}