diff --git a/src/main/java/meteordevelopment/meteorclient/gui/screens/ContainerInventoryScreen.java b/src/main/java/meteordevelopment/meteorclient/gui/screens/ContainerInventoryScreen.java new file mode 100644 index 0000000000..ebcf6b271c --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/gui/screens/ContainerInventoryScreen.java @@ -0,0 +1,176 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.gui.screens; + +import meteordevelopment.meteorclient.systems.modules.Modules; +import meteordevelopment.meteorclient.systems.modules.render.BetterTooltips; +import meteordevelopment.meteorclient.utils.Utils; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.BundleContentsComponent; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.BundleItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static meteordevelopment.meteorclient.MeteorClient.mc; + +/* + * i couldn't figure out how to add proper outer borders for the GUI without adding custom textures. @TODO + */ +public class ContainerInventoryScreen extends Screen { + private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot"); + private static final int SLOT_SIZE = 18; + private static final int SCREEN_WIDTH = 176; + + private final List containerItems; + private final PlayerInventory playerInventory; + private final int containerRows; + private int x, y; + + private int baseX, baseY; + private int playerY; + + public ContainerInventoryScreen(ItemStack containerItem) { + super(containerItem.getName()); + this.playerInventory = mc.player.getInventory(); + + this.containerItems = new ArrayList<>(); + if (containerItem.getItem() instanceof BundleItem) { + BundleContentsComponent bundleContents = containerItem.get(DataComponentTypes.BUNDLE_CONTENTS); + if (bundleContents != null) { + bundleContents.iterate().forEach(containerItems::add); + } + } else { + ItemStack[] tempItems = new ItemStack[64]; + Utils.getItemsInContainerItem(containerItem, tempItems); + Collections.addAll(containerItems, tempItems); + } + + this.containerRows = Math.max(1, MathHelper.ceilDiv(containerItems.size(), 9)); + } + + @Override + protected void init() { + super.init(); + this.x = (this.width - SCREEN_WIDTH) / 2; + this.y = (this.height - (114 + containerRows * SLOT_SIZE + 20)) / 2; + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + + baseX = x + 8; + baseY = y + 18; + playerY = baseY + containerRows * SLOT_SIZE + 20; + + // drawing the slot textures + for (int row = 0; row < containerRows + 4; row++) { + for (int col = 0; col < 9; col++) { + int slotY = row < containerRows ? baseY + row * SLOT_SIZE : playerY + (row - containerRows) * SLOT_SIZE; + context.drawGuiTexture(RenderPipelines.GUI_TEXTURED, SLOT_TEXTURE, baseX + col * SLOT_SIZE, slotY, SLOT_SIZE, SLOT_SIZE); + } + } + + // drawing the container items + for (int i = 0; i < containerItems.size(); i++) { + ItemStack item = containerItems.get(i); + if (!item.isEmpty()) { + int itemX = baseX + (i % 9) * SLOT_SIZE + 1; + int itemY = baseY + (i / 9) * SLOT_SIZE + 1; + context.drawItem(item, itemX, itemY); + context.drawStackOverlay(textRenderer, item, itemX, itemY); + } + } + + // drawing your inventory items + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 9; col++) { + int slotIndex = row < 3 ? 9 + row * 9 + col : col; + ItemStack item = playerInventory.getStack(slotIndex); + if (!item.isEmpty()) { + int itemX = baseX + col * SLOT_SIZE + 1; + int itemY = playerY + row * SLOT_SIZE + 1; + context.drawItem(item, itemX, itemY); + context.drawStackOverlay(textRenderer, item, itemX, itemY); + } + } + } + + // drawing title headers + context.getMatrices().pushMatrix(); + context.getMatrices().translate((float) x, (float) y); + if (textRenderer != null) { + context.drawText(textRenderer, title, 8, 6, -12566464, false); + context.drawText(textRenderer, playerInventory.getDisplayName(), 8, 18 + containerRows * SLOT_SIZE + 10, -12566464, false); + } + context.getMatrices().popMatrix(); + + // drawing the tooltip + ItemStack item = getSelectedItem(mouseX, mouseY); + if (!item.isEmpty()) { + context.drawTooltip(textRenderer, getTooltipFromItem(mc, item), item.getTooltipData(), mouseX, mouseY); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + BetterTooltips tooltips = Modules.get().get(BetterTooltips.class); + + ItemStack stack = getSelectedItem((int) mouseX, (int) mouseY); + if (tooltips.shouldOpenContents(false, button, 0)) { + return tooltips.openContent(stack); + } + + return false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + BetterTooltips tooltips = Modules.get().get(BetterTooltips.class); + + ItemStack stack = getSelectedItem((int) mc.mouse.getScaledX(mc.getWindow()), (int) mc.mouse.getScaledY(mc.getWindow())); + if (tooltips.shouldOpenContents(true, keyCode, modifiers)) { + return tooltips.openContent(stack); + } + + if (keyCode == GLFW.GLFW_KEY_ESCAPE || mc.options.inventoryKey.matchesKey(keyCode, scanCode)) { + close(); + return true; + } + + return false; + } + + private ItemStack getSelectedItem(int mouseX, int mouseY) { + if (mouseX < baseX || mouseX > baseX + 9 * SLOT_SIZE) return ItemStack.EMPTY; + + int col = (mouseX - baseX) / SLOT_SIZE; + if (col > 8) return ItemStack.EMPTY; + + if (mouseY >= baseY && mouseY < baseY + containerRows * SLOT_SIZE) { + int index = ((mouseY - baseY) / SLOT_SIZE) * 9 + col; + return (index < containerItems.size() ? containerItems.get(index) : ItemStack.EMPTY); + } + + if (mouseY >= playerY && mouseY < playerY + 4 * SLOT_SIZE) { + int row = (mouseY - playerY) / SLOT_SIZE; + int slotIndex = row < 3 ? 9 + row * 9 + col : col; + return playerInventory.getStack(slotIndex); + } + + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/BundleItemMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/BundleItemMixin.java new file mode 100644 index 0000000000..15fa22b4f2 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/mixin/BundleItemMixin.java @@ -0,0 +1,29 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.mixin; + +import meteordevelopment.meteorclient.MeteorClient; +import meteordevelopment.meteorclient.events.render.TooltipDataEvent; +import net.minecraft.item.BundleItem; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Optional; + +@Mixin(BundleItem.class) +public class BundleItemMixin { + @Inject(method = "getTooltipData", at = @At("HEAD"), cancellable = true) + private void onTooltipData(ItemStack stack, CallbackInfoReturnable> cir) { + TooltipDataEvent event = MeteorClient.EVENT_BUS.post(TooltipDataEvent.get(stack)); + if (event.tooltipData != null) { + cir.setReturnValue(Optional.of(event.tooltipData)); + } + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/BundleTooltipSubmenuHandlerMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/BundleTooltipSubmenuHandlerMixin.java new file mode 100644 index 0000000000..8c3a7183f6 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/mixin/BundleTooltipSubmenuHandlerMixin.java @@ -0,0 +1,31 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import meteordevelopment.meteorclient.systems.modules.Modules; +import meteordevelopment.meteorclient.systems.modules.misc.InventoryTweaks; +import net.minecraft.client.gui.tooltip.BundleTooltipSubmenuHandler; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.BundleContentsComponent; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(BundleTooltipSubmenuHandler.class) +public class BundleTooltipSubmenuHandlerMixin { + @ModifyExpressionValue(method = "sendPacket", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/BundleItem;getNumberOfStacksShown(Lnet/minecraft/item/ItemStack;)I")) + private int uncapBundleScrolling1(int original, ItemStack item, int slotId, int selectedItemIndex) { + if (Modules.get().get(InventoryTweaks.class).uncapBundleScrolling()) return item.getOrDefault(DataComponentTypes.BUNDLE_CONTENTS, BundleContentsComponent.DEFAULT).size(); + return original; + } + + @ModifyExpressionValue(method = "onScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/BundleItem;getNumberOfStacksShown(Lnet/minecraft/item/ItemStack;)I")) + private int uncapBundleScrolling2(int original, double horizontal, double vertical, int slotId, ItemStack item) { + if (Modules.get().get(InventoryTweaks.class).uncapBundleScrolling()) return item.getOrDefault(DataComponentTypes.BUNDLE_CONTENTS, BundleContentsComponent.DEFAULT).size(); + return original; + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/HandledScreenMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/HandledScreenMixin.java index a668028284..2d4aedabf1 100644 --- a/src/main/java/meteordevelopment/meteorclient/mixin/HandledScreenMixin.java +++ b/src/main/java/meteordevelopment/meteorclient/mixin/HandledScreenMixin.java @@ -5,35 +5,30 @@ package meteordevelopment.meteorclient.mixin; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import meteordevelopment.meteorclient.systems.modules.Modules; import meteordevelopment.meteorclient.systems.modules.misc.InventoryTweaks; import meteordevelopment.meteorclient.systems.modules.render.BetterTooltips; import meteordevelopment.meteorclient.systems.modules.render.ItemHighlight; -import meteordevelopment.meteorclient.utils.Utils; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.ingame.BookScreen; import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.client.gui.screen.ingame.ScreenHandlerProvider; +import net.minecraft.client.gui.tooltip.TooltipComponent; import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.component.DataComponentTypes; import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; import net.minecraft.screen.ScreenHandler; import net.minecraft.screen.slot.Slot; import net.minecraft.screen.slot.SlotActionType; import net.minecraft.text.Text; import org.jetbrains.annotations.Nullable; -import org.lwjgl.glfw.GLFW; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import static meteordevelopment.meteorclient.MeteorClient.mc; import static org.lwjgl.glfw.GLFW.GLFW_MOUSE_BUTTON_LEFT; @Mixin(HandledScreen.class) @@ -62,9 +57,6 @@ public abstract class HandledScreenMixin extends Screen @Shadow public abstract void close(); - @Unique - private static final ItemStack[] ITEMS = new ItemStack[27]; - public HandledScreenMixin(Text title) { super(title); } @@ -104,14 +96,20 @@ private void onMouseDragged(double mouseX, double mouseY, int button, double del private void mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { BetterTooltips tooltips = Modules.get().get(BetterTooltips.class); - if (button == GLFW.GLFW_MOUSE_BUTTON_MIDDLE && focusedSlot != null && !focusedSlot.getStack().isEmpty() && getScreenHandler().getCursorStack().isEmpty() && tooltips.middleClickOpen()) { - ItemStack itemStack = focusedSlot.getStack(); - if (Utils.hasItems(itemStack) || itemStack.getItem() == Items.ENDER_CHEST) { - cir.setReturnValue(Utils.openContainer(focusedSlot.getStack(), ITEMS, false)); + if (tooltips.shouldOpenContents(false, button, 0) && focusedSlot != null && !focusedSlot.getStack().isEmpty() && getScreenHandler().getCursorStack().isEmpty()) { + if (tooltips.openContent(focusedSlot.getStack())) { + cir.setReturnValue(true); } - else if (itemStack.get(DataComponentTypes.WRITTEN_BOOK_CONTENT) != null || itemStack.get(DataComponentTypes.WRITABLE_BOOK_CONTENT) != null) { - close(); - mc.setScreen(new BookScreen(BookScreen.Contents.create(itemStack))); + } + } + + // Keyboard input for middle click open + @Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true) + private void keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable cir) { + BetterTooltips tooltips = Modules.get().get(BetterTooltips.class); + + if (tooltips.shouldOpenContents(true, keyCode, modifiers) && focusedSlot != null && !focusedSlot.getStack().isEmpty() && getScreenHandler().getCursorStack().isEmpty()) { + if (tooltips.openContent(focusedSlot.getStack())) { cir.setReturnValue(true); } } @@ -123,4 +121,13 @@ private void onDrawSlot(DrawContext context, Slot slot, CallbackInfo ci) { int color = Modules.get().get(ItemHighlight.class).getColor(slot.getStack()); if (color != -1) context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, color); } + + @ModifyReturnValue(method = "isItemTooltipSticky", at = @At("RETURN")) + private boolean isTooltipSticky(boolean original, ItemStack item) { + if (item.getTooltipData().orElse(null) instanceof TooltipComponent component) { + return original || component.isSticky(); + } + + return original; + } } diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/ItemMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/ItemMixin.java index cfd2422820..058adad1e4 100644 --- a/src/main/java/meteordevelopment/meteorclient/mixin/ItemMixin.java +++ b/src/main/java/meteordevelopment/meteorclient/mixin/ItemMixin.java @@ -7,7 +7,6 @@ import meteordevelopment.meteorclient.MeteorClient; import meteordevelopment.meteorclient.events.render.TooltipDataEvent; - import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.tooltip.TooltipData; @@ -20,7 +19,7 @@ @Mixin(Item.class) public abstract class ItemMixin { - @Inject(method = "getTooltipData", at=@At("HEAD"), cancellable = true) + @Inject(method = "getTooltipData", at = @At("HEAD"), cancellable = true) private void onTooltipData(ItemStack stack, CallbackInfoReturnable> cir) { TooltipDataEvent event = MeteorClient.EVENT_BUS.post(TooltipDataEvent.get(stack)); if (event.tooltipData != null) { diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/InventoryTweaks.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/InventoryTweaks.java index d9b4106e49..7721709e74 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/InventoryTweaks.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/InventoryTweaks.java @@ -104,6 +104,13 @@ public class InventoryTweaks extends Module { .build() ); + private final Setting uncapBundleScrolling = sgGeneral.add(new BoolSetting.Builder() + .name("uncap-bundle-scrolling") + .description("Whether to uncap the bundle scrolling feature to let you select any item.") + .defaultValue(true) + .build() + ); + // Anti drop private final Setting> antiDropItems = sgAntiDrop.add(new ItemListSetting.Builder() @@ -452,6 +459,10 @@ public boolean mouseDragItemMove() { return isActive() && mouseDragItemMove.get(); } + public boolean uncapBundleScrolling() { + return isActive() && uncapBundleScrolling.get(); + } + public boolean canSteal(ScreenHandler handler) { try { return (stealScreens.get().contains(handler.getType())); diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java index 06fe0da2fa..6f7345236a 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java @@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import meteordevelopment.meteorclient.events.game.ItemStackTooltipEvent; import meteordevelopment.meteorclient.events.render.TooltipDataEvent; +import meteordevelopment.meteorclient.gui.screens.ContainerInventoryScreen; import meteordevelopment.meteorclient.mixin.EntityAccessor; import meteordevelopment.meteorclient.mixin.EntityBucketItemAccessor; import meteordevelopment.meteorclient.settings.*; @@ -22,6 +23,8 @@ import meteordevelopment.meteorclient.utils.render.color.Color; import meteordevelopment.meteorclient.utils.tooltip.*; import meteordevelopment.orbit.EventHandler; +import net.minecraft.client.gui.screen.ingame.BookScreen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.*; import net.minecraft.component.type.SuspiciousStewEffectsComponent.StewEffect; @@ -47,6 +50,7 @@ import java.util.function.Consumer; import static org.lwjgl.glfw.GLFW.GLFW_KEY_LEFT_ALT; +import static org.lwjgl.glfw.GLFW.GLFW_MOUSE_BUTTON_MIDDLE; public class BetterTooltips extends Module { public static final Color ECHEST_COLOR = new Color(0, 50, 50); @@ -74,18 +78,26 @@ public class BetterTooltips extends Module { .build() ); - private final Setting middleClickOpen = sgGeneral.add(new BoolSetting.Builder() - .name("middle-click-open") - .description("Opens a GUI window with the inventory of the storage block or book when you middle click the item.") + private final Setting openContents = sgGeneral.add(new BoolSetting.Builder() + .name("open-contents") + .description("Opens a GUI window with the inventory of the storage block or book when you click the item.") .defaultValue(true) .build() ); + private final Setting openContentsKey = sgGeneral.add(new KeybindSetting.Builder() + .name("keybind") + .description("Key to open contents (containers, books, etc.) when pressed on items.") + .defaultValue(Keybind.fromButton(GLFW_MOUSE_BUTTON_MIDDLE)) + .visible(openContents::get) + .build() + ); + private final Setting pauseInCreative = sgGeneral.add(new BoolSetting.Builder() .name("pause-in-creative") .description("Pauses middle click open while the player is in creative mode.") .defaultValue(true) - .visible(middleClickOpen::get) + .visible(openContents::get) .build() ); @@ -156,6 +168,22 @@ public class BetterTooltips extends Module { .build() ); + private final Setting bundles = sgPreviews.add(new BoolSetting.Builder() + .name("bundles") + .description("Shows a preview of bundle contents when hovering over it in an inventory.") + .defaultValue(true) + .onChanged(value -> updateTooltips = true) + .build() + ); + + private final Setting foodInfo = sgPreviews.add(new BoolSetting.Builder() + .name("food-info") + .description("Shows hunger and saturation values for food items.") + .defaultValue(true) + .onChanged(value -> updateTooltips = true) + .build() + ); + // Extras public final Setting byteSize = sgOther.add(new BoolSetting.Builder() @@ -199,7 +227,8 @@ public class BetterTooltips extends Module { ); private boolean updateTooltips = false; - private static final ItemStack[] ITEMS = new ItemStack[27]; + private static final ItemStack[] PREVIEW = new ItemStack[27]; + private static final ItemStack[] PEEK_SCREEN = new ItemStack[27]; public BetterTooltips() { super(Categories.Render, "better-tooltips", "Displays more useful tooltips for certain items."); @@ -236,6 +265,13 @@ private void appendTooltip(ItemStackTooltipEvent event) { } } + // Food info + if (foodInfo.get() && event.itemStack().contains(DataComponentTypes.FOOD)) { + FoodComponent food = event.itemStack().get(DataComponentTypes.FOOD); + // Those emojis really look like in-game hunger bar + event.appendStart(Text.literal(String.format("🍖 %d (💛 %.1f)", food.nutrition(), food.saturation())).formatted(Formatting.GRAY)); + } + // Item size tooltip if (byteSize.get()) { switch (ItemStack.CODEC.encodeStart(mc.player.getRegistryManager().getOps(NbtOps.INSTANCE), event.itemStack())) { @@ -277,8 +313,8 @@ private void appendTooltip(ItemStackTooltipEvent event) { private void getTooltipData(TooltipDataEvent event) { // Container preview if (previewShulkers() && Utils.hasItems(event.itemStack)) { - Utils.getItemsInContainerItem(event.itemStack, ITEMS); - event.tooltipData = new ContainerTooltipComponent(ITEMS, Utils.getShulkerColor(event.itemStack)); + Utils.getItemsInContainerItem(event.itemStack, PREVIEW); + event.tooltipData = new ContainerTooltipComponent(PREVIEW, Utils.getShulkerColor(event.itemStack)); } // EChest preview @@ -297,7 +333,11 @@ else if (event.itemStack.getItem() == Items.FILLED_MAP && previewMaps()) { // Book preview else if ((event.itemStack.getItem() == Items.WRITABLE_BOOK || event.itemStack.getItem() == Items.WRITTEN_BOOK) && previewBooks()) { Text page = getFirstPage(event.itemStack); - if (page != null) event.tooltipData = new BookTooltipComponent(page); + if (page != null) { + int pageCount = getBookPageCount(event.itemStack); + Text pageWithCount = page.copy().append(Text.literal(String.format(" (%d pages)", pageCount)).formatted(Formatting.GRAY)); + event.tooltipData = new BookTooltipComponent(pageWithCount); + } } // Banner preview @@ -328,6 +368,21 @@ else if (event.itemStack.getItem() instanceof EntityBucketItem bucketItem && pre event.tooltipData = new EntityTooltipComponent(entity); } } + + // Bundle preview + else if (event.itemStack.getItem() instanceof BundleItem && previewBundles()) { + if (event.itemStack.contains(DataComponentTypes.BUNDLE_CONTENTS)) { + BundleContentsComponent bundleContents = event.itemStack.get(DataComponentTypes.BUNDLE_CONTENTS); + if (bundleContents != null && !bundleContents.isEmpty()) { + ItemStack[] bundleItems = new ItemStack[bundleContents.size()]; + int index = 0; + for (ItemStack stack : bundleContents.iterate()) { + bundleItems[index++] = stack; + } + event.tooltipData = new BundleTooltipComponent(bundleItems, bundleContents); + } + } + } } public void applyCompactShulkerTooltip(List stacks, Consumer textConsumer) { @@ -352,17 +407,20 @@ public void applyCompactShulkerTooltip(List stacks, Consumer te } private void appendPreviewTooltipText(ItemStackTooltipEvent event, boolean spacer) { - if (!isPressed() && ( + boolean showPreviewText = !isPressed() && ( shulkers.get() && Utils.hasItems(event.itemStack()) || (event.itemStack().getItem() == Items.ENDER_CHEST && echest.get()) || (event.itemStack().getItem() == Items.FILLED_MAP && maps.get()) || (event.itemStack().getItem() == Items.WRITABLE_BOOK && books.get()) || (event.itemStack().getItem() == Items.WRITTEN_BOOK && books.get()) || (event.itemStack().getItem() instanceof EntityBucketItem && entitiesInBuckets.get()) + || (event.itemStack().getItem() instanceof BundleItem && bundles.get()) || (event.itemStack().getItem() instanceof BannerItem && banners.get()) || (event.itemStack().contains(DataComponentTypes.PROVIDES_BANNER_PATTERNS) && banners.get()) || (event.itemStack().getItem() == Items.SHIELD && banners.get()) - )) { + ); + + if (showPreviewText) { // we don't want to add the spacer if the tooltip is hidden if (spacer) event.appendEnd(Text.literal("")); event.appendEnd(Text.literal("Hold " + Formatting.YELLOW + keybind + Formatting.RESET + " to preview")); @@ -398,6 +456,15 @@ private Text getFirstPage(ItemStack bookItem) { return null; } + private int getBookPageCount(ItemStack bookItem) { + if (bookItem.get(DataComponentTypes.WRITABLE_BOOK_CONTENT) != null) { + return bookItem.get(DataComponentTypes.WRITABLE_BOOK_CONTENT).pages().size(); + } else if (bookItem.get(DataComponentTypes.WRITTEN_BOOK_CONTENT) != null) { + return bookItem.get(DataComponentTypes.WRITTEN_BOOK_CONTENT).pages().size(); + } + return 0; + } + private BannerTooltipComponent createBannerFromBannerPatternItem(ItemStack item) { // I can't imagine getting the banner pattern from a banner pattern item would fail without some serious messing around BannerPatternsComponent component = new BannerPatternsComponent.Builder().add(mc.player.getRegistryManager().getOrThrow(RegistryKeys.BANNER_PATTERN).getOrThrow(item.get(DataComponentTypes.PROVIDES_BANNER_PATTERNS)).get(0), DyeColor.WHITE).build(); @@ -410,8 +477,31 @@ private BannerTooltipComponent createBannerFromShield(ItemStack shieldItem) { return new BannerTooltipComponent(dyeColor2, bannerPatternsComponent); } - public boolean middleClickOpen() { - return (isActive() && middleClickOpen.get()) && (!pauseInCreative.get() || !mc.player.isInCreativeMode()); + public boolean openContents() { + return (isActive() && openContents.get()) && (!pauseInCreative.get() || !mc.player.isInCreativeMode()); + } + + public boolean shouldOpenContents(boolean isKey, int keycode, int modifiers) { + return openContents() && openContentsKey.get().matches(isKey, keycode, modifiers); + } + + public boolean openContent(ItemStack itemStack) { + if (!openContents() || itemStack.isEmpty()) return false; + + if (itemStack.getItem() instanceof BundleItem) { + if (mc.currentScreen instanceof HandledScreen) mc.currentScreen.close(); + mc.setScreen(new ContainerInventoryScreen(itemStack)); + return true; + } else if (Utils.hasItems(itemStack) || itemStack.getItem() == Items.ENDER_CHEST) { + Utils.openContainer(itemStack, PEEK_SCREEN, false); + return true; + } else if (itemStack.getItem() == Items.WRITABLE_BOOK || itemStack.getItem() == Items.WRITTEN_BOOK) { + if (mc.currentScreen instanceof HandledScreen) mc.currentScreen.close(); + mc.setScreen(new BookScreen(BookScreen.Contents.create(itemStack))); + return true; + } + + return false; } public boolean previewShulkers() { @@ -442,6 +532,10 @@ private boolean previewEntities() { return isPressed() && entitiesInBuckets.get(); } + public boolean previewBundles() { + return isPressed() && bundles.get(); + } + private boolean isPressed() { return (keybind.get().isPressed() && displayWhen.get() == DisplayWhen.Keybind) || displayWhen.get() == DisplayWhen.Always; } diff --git a/src/main/java/meteordevelopment/meteorclient/utils/Utils.java b/src/main/java/meteordevelopment/meteorclient/utils/Utils.java index a5a0ebffbe..a316bd8b2a 100644 --- a/src/main/java/meteordevelopment/meteorclient/utils/Utils.java +++ b/src/main/java/meteordevelopment/meteorclient/utils/Utils.java @@ -283,7 +283,7 @@ else if (components.contains(DataComponentTypes.BLOCK_ENTITY_DATA)) { if (slot.get() >= 0 && slot.get() < items.length) { switch (StackWithSlot.CODEC.parse(mc.player.getRegistryManager().getOps(NbtOps.INSTANCE), compound.get())) { case DataResult.Success success -> items[slot.get()] = success.value().stack(); - case DataResult.Error error -> items[slot.get()] = ItemStack.EMPTY; + case DataResult.Error ignored -> items[slot.get()] = ItemStack.EMPTY; default -> throw new MatchException(null, null); } } diff --git a/src/main/java/meteordevelopment/meteorclient/utils/render/PeekScreen.java b/src/main/java/meteordevelopment/meteorclient/utils/render/PeekScreen.java index 3c0a232934..ece9dc6f53 100644 --- a/src/main/java/meteordevelopment/meteorclient/utils/render/PeekScreen.java +++ b/src/main/java/meteordevelopment/meteorclient/utils/render/PeekScreen.java @@ -11,12 +11,9 @@ import meteordevelopment.meteorclient.utils.render.color.Color; import net.minecraft.client.gl.RenderPipelines; import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.gui.screen.ingame.BookScreen; import net.minecraft.client.gui.screen.ingame.ShulkerBoxScreen; -import net.minecraft.component.DataComponentTypes; import net.minecraft.inventory.SimpleInventory; import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; import net.minecraft.screen.ShulkerBoxScreenHandler; import net.minecraft.util.Identifier; import net.minecraft.util.math.ColorHelper; @@ -26,12 +23,10 @@ public class PeekScreen extends ShulkerBoxScreen { private final Identifier TEXTURE = Identifier.of("textures/gui/container/shulker_box.png"); - private final ItemStack[] contents; private final ItemStack storageBlock; public PeekScreen(ItemStack storageBlock, ItemStack[] contents) { super(new ShulkerBoxScreenHandler(0, mc.player.getInventory(), new SimpleInventory(contents)), mc.player.getInventory(), storageBlock.getName()); - this.contents = contents; this.storageBlock = storageBlock; } @@ -39,15 +34,9 @@ public PeekScreen(ItemStack storageBlock, ItemStack[] contents) { public boolean mouseClicked(double mouseX, double mouseY, int button) { BetterTooltips tooltips = Modules.get().get(BetterTooltips.class); - if (button == GLFW.GLFW_MOUSE_BUTTON_MIDDLE && focusedSlot != null && !focusedSlot.getStack().isEmpty() && mc.player.currentScreenHandler.getCursorStack().isEmpty() && tooltips.middleClickOpen()) { + if (tooltips.shouldOpenContents(false, button, 0) && focusedSlot != null && !focusedSlot.getStack().isEmpty() && mc.player.currentScreenHandler.getCursorStack().isEmpty()) { ItemStack itemStack = focusedSlot.getStack(); - if (Utils.hasItems(itemStack) || itemStack.getItem() == Items.ENDER_CHEST) { - return Utils.openContainer(focusedSlot.getStack(), contents, false); - } else if (itemStack.get(DataComponentTypes.WRITTEN_BOOK_CONTENT) != null || itemStack.get(DataComponentTypes.WRITABLE_BOOK_CONTENT) != null) { - close(); - mc.setScreen(new BookScreen(BookScreen.Contents.create(itemStack))); - return true; - } + return tooltips.openContent(itemStack); } return false; @@ -60,19 +49,20 @@ public boolean mouseReleased(double mouseX, double mouseY, int button) { @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (keyCode == GLFW.GLFW_KEY_ESCAPE || mc.options.inventoryKey.matchesKey(keyCode, scanCode)) { - close(); - return true; + BetterTooltips tooltips = Modules.get().get(BetterTooltips.class); + + if (tooltips.shouldOpenContents(true, keyCode, modifiers) && focusedSlot != null && !focusedSlot.getStack().isEmpty() && mc.player.currentScreenHandler.getCursorStack().isEmpty()) { + ItemStack itemStack = focusedSlot.getStack(); + if (tooltips.openContent(itemStack)) { + return true; + } } - return false; - } - @Override - public boolean keyReleased(int keyCode, int scanCode, int modifiers) { - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE || mc.options.inventoryKey.matchesKey(keyCode, scanCode)) { close(); return true; } + return false; } @@ -82,6 +72,6 @@ protected void drawBackground(DrawContext context, float delta, int mouseX, int int i = (width - backgroundWidth) / 2; int j = (height - backgroundHeight) / 2; - context.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, i, j, 0f, 0f, backgroundWidth, backgroundHeight, backgroundWidth, backgroundHeight, 256, 256, ColorHelper.fromFloats(color.r / 255f, color.g / 255f, color.b / 255f, color.a / 255f)); + context.drawTexture(RenderPipelines.GUI_TEXTURED, TEXTURE, i, j, 0f, 0f, backgroundWidth, backgroundHeight, backgroundWidth, backgroundHeight, 256, 256, ColorHelper.fromFloats(color.a / 255f, color.r / 255f, color.g / 255f, color.b / 255f)); } } diff --git a/src/main/java/meteordevelopment/meteorclient/utils/tooltip/BundleTooltipComponent.java b/src/main/java/meteordevelopment/meteorclient/utils/tooltip/BundleTooltipComponent.java new file mode 100644 index 0000000000..be0e7c5e79 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/tooltip/BundleTooltipComponent.java @@ -0,0 +1,149 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.tooltip; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.BundleContentsComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.apache.commons.lang3.math.Fraction; + +import java.util.List; + +public class BundleTooltipComponent implements TooltipComponent, MeteorTooltipData { + private static final Identifier BUNDLE_SLOT_BACKGROUND_TEXTURE = Identifier.ofVanilla("container/bundle/slot_background"); + private static final Identifier BUNDLE_PROGRESS_BAR_BORDER_TEXTURE = Identifier.ofVanilla("container/bundle/bundle_progressbar_border"); + private static final Identifier BUNDLE_PROGRESS_BAR_FILL_TEXTURE = Identifier.ofVanilla("container/bundle/bundle_progressbar_fill"); + private static final Identifier BUNDLE_PROGRESS_BAR_FULL_TEXTURE = Identifier.ofVanilla("container/bundle/bundle_progressbar_full"); + private static final Identifier BUNDLE_SLOT_HIGHLIGHT_BACK_TEXTURE = Identifier.ofVanilla("container/bundle/slot_highlight_back"); + private static final Identifier BUNDLE_SLOT_HIGHLIGHT_FRONT_TEXTURE = Identifier.ofVanilla("container/bundle/slot_highlight_front"); + + private static final int SLOTS_PER_ROW = 8; + private static final int SLOT_DIMENSION = 24; + private static final int ROW_WIDTH = 8 + SLOTS_PER_ROW * SLOT_DIMENSION + 8; + private static final int PROGRESS_BAR_WIDTH = 94; + private static final int PROGRESS_BAR_HEIGHT = 13; + private static final Text BUNDLE_FULL = Text.translatable("item.minecraft.bundle.full"); + + private final ItemStack[] items; + private final BundleContentsComponent bundleContents; + private final int width; + private final int height; + + public BundleTooltipComponent(ItemStack[] items, BundleContentsComponent bundleContents) { + this.items = items; + this.bundleContents = bundleContents; + + int rows = (items.length + SLOTS_PER_ROW - 1) / SLOTS_PER_ROW; + this.width = ROW_WIDTH; + this.height = 8 + rows * SLOT_DIMENSION + 8 + PROGRESS_BAR_HEIGHT + 4; + } + + @Override + public TooltipComponent getComponent() { + return this; + } + + @Override + public int getHeight(TextRenderer textRenderer) { + return height; + } + + @Override + public int getWidth(TextRenderer textRenderer) { + return width; + } + + @Override + public boolean isSticky() { + return true; + } + + @Override + public void drawItems(TextRenderer textRenderer, int x, int y, int width, int height, DrawContext context) { + int row = 0; + int col = 0; + + for (ItemStack itemStack : items) { + if (!itemStack.isEmpty()) { + int slotX = x + 8 + col * SLOT_DIMENSION; + int slotY = y + 8 + row * SLOT_DIMENSION; + + context.drawGuiTexture(RenderPipelines.GUI_TEXTURED, BUNDLE_SLOT_BACKGROUND_TEXTURE, slotX, slotY, SLOT_DIMENSION, SLOT_DIMENSION); + drawItem(itemStack, (row * 8) + col, slotX, slotY, textRenderer, context); + context.drawStackOverlay(textRenderer, itemStack, slotX + 4, slotY + 4); + } + + col++; + if (col >= SLOTS_PER_ROW) { + col = 0; + row++; + } + } + + drawSelectedItemTooltip(textRenderer, context, x, y, width); + + int progressBarX = x + (this.width - PROGRESS_BAR_WIDTH) / 2; + int progressBarY = y + this.height - PROGRESS_BAR_HEIGHT - 4; + drawProgressBar(progressBarX, progressBarY, textRenderer, context); + } + + private void drawItem(ItemStack itemStack, int index, int x, int y, TextRenderer textRenderer, DrawContext drawContext) { + boolean bl = bundleContents.getSelectedStackIndex() == index; + if (bl) { + drawContext.drawGuiTexture(RenderPipelines.GUI_TEXTURED, BUNDLE_SLOT_HIGHLIGHT_BACK_TEXTURE, x, y, 24, 24); + } else { + drawContext.drawGuiTexture(RenderPipelines.GUI_TEXTURED, BUNDLE_SLOT_BACKGROUND_TEXTURE, x, y, 24, 24); + } + + drawContext.drawItem(itemStack, x + 4, y + 4, 0); + drawContext.drawStackOverlay(textRenderer, itemStack, x + 4, y + 4); + if (bl) { + drawContext.drawGuiTexture(RenderPipelines.GUI_TEXTURED, BUNDLE_SLOT_HIGHLIGHT_FRONT_TEXTURE, x, y, 24, 24); + } + } + + private void drawSelectedItemTooltip(TextRenderer textRenderer, DrawContext drawContext, int x, int y, int width) { + if (this.bundleContents.hasSelectedStack()) { + ItemStack itemStack = this.bundleContents.get(this.bundleContents.getSelectedStackIndex()); + Text text = itemStack.getFormattedName(); + int i = textRenderer.getWidth(text.asOrderedText()); + int j = x + width / 2 - 12; + TooltipComponent tooltipComponent = TooltipComponent.of(text.asOrderedText()); + drawContext.drawTooltipImmediately( + textRenderer, List.of(tooltipComponent), j - i / 2, y - 37, HoveredTooltipPositioner.INSTANCE, itemStack.get(DataComponentTypes.TOOLTIP_STYLE) + ); + } + } + + private void drawProgressBar(int x, int y, TextRenderer textRenderer, DrawContext context) { + int fillAmount = MathHelper.clamp(MathHelper.multiplyFraction(bundleContents.getOccupancy(), PROGRESS_BAR_WIDTH), 0, PROGRESS_BAR_WIDTH); + + Identifier fillTexture = bundleContents.getOccupancy().compareTo(Fraction.ONE) >= 0 + ? BUNDLE_PROGRESS_BAR_FULL_TEXTURE + : BUNDLE_PROGRESS_BAR_FILL_TEXTURE; + + context.drawGuiTexture(RenderPipelines.GUI_TEXTURED, fillTexture, x + 1, y, fillAmount, PROGRESS_BAR_HEIGHT); + context.drawGuiTexture(RenderPipelines.GUI_TEXTURED, BUNDLE_PROGRESS_BAR_BORDER_TEXTURE, x, y, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT); + + Text label = getProgressBarLabel(); + if (label != null) { + context.drawCenteredTextWithShadow(textRenderer, label, x + PROGRESS_BAR_WIDTH / 2, y + 3, Colors.WHITE); + } + } + + private Text getProgressBarLabel() { + return bundleContents.getOccupancy().compareTo(Fraction.ONE) >= 0 ? BUNDLE_FULL : Text.literal(String.format("%.2f%%", bundleContents.getOccupancy().floatValue() * 100)); + } +} diff --git a/src/main/resources/meteor-client.mixins.json b/src/main/resources/meteor-client.mixins.json index 44aea0cd37..4d473d7af2 100644 --- a/src/main/resources/meteor-client.mixins.json +++ b/src/main/resources/meteor-client.mixins.json @@ -36,6 +36,8 @@ "BoxMixin", "BrewingStandScreenMixin", "BrightnessGetterMixin", + "BundleItemMixin", + "BundleTooltipSubmenuHandlerMixin", "CameraMixin", "CapabilityTrackerMixin", "CapeFeatureRendererMixin",