diff --git a/src/main/java/com/lambda/mixin/items/BlockItemMixin.java b/src/main/java/com/lambda/mixin/items/BlockItemMixin.java new file mode 100644 index 000000000..c599cea55 --- /dev/null +++ b/src/main/java/com/lambda/mixin/items/BlockItemMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.items; + +import com.lambda.module.modules.render.ContainerPreview; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipData; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.Optional; + +@Mixin(BlockItem.class) +public class BlockItemMixin extends Item { + public BlockItemMixin(Settings settings) { + super(settings); + } + + @Override + public Optional getTooltipData(ItemStack stack) { + if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isPreviewableContainer(stack)) { + return Optional.of(new ContainerPreview.ContainerComponent(stack)); + } + return super.getTooltipData(stack); + } +} diff --git a/src/main/java/com/lambda/mixin/render/DrawContextMixin.java b/src/main/java/com/lambda/mixin/render/DrawContextMixin.java new file mode 100644 index 000000000..825e8b9a0 --- /dev/null +++ b/src/main/java/com/lambda/mixin/render/DrawContextMixin.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.module.modules.render.ContainerPreview; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.client.gui.tooltip.TooltipPositioner; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipData; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; +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.CallbackInfo; + +import java.util.List; +import java.util.Optional; + +@Mixin(DrawContext.class) +public class DrawContextMixin { + @Inject(method = "drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;IILnet/minecraft/util/Identifier;)V", at = @At("HEAD"), cancellable = true) + private void onDrawTooltip(TextRenderer textRenderer, List text, Optional data, int x, int y, @Nullable Identifier texture, CallbackInfo ci) { + if (!ContainerPreview.INSTANCE.isEnabled()) return; + + if (ContainerPreview.isRenderingSubTooltip()) return; + + if (ContainerPreview.isLocked()) { + ci.cancel(); + ContainerPreview.renderLockedTooltip((DrawContext)(Object)this, textRenderer); + return; + } + + if (data.isPresent() && data.get() instanceof ContainerPreview.ContainerComponent component) { + ci.cancel(); + ContainerPreview.renderShulkerTooltip((DrawContext)(Object)this, textRenderer, component, x, y); + } + } +} diff --git a/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java b/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java new file mode 100644 index 000000000..193cdc516 --- /dev/null +++ b/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.module.modules.render.ContainerPreview; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +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; + +@Mixin(HandledScreen.class) +public class HandledScreenMixin { + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + private void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) { + if (ContainerPreview.isMouseOverLockedTooltip((int) mouseX, (int) mouseY)) { + cir.setReturnValue(true); + } + } + } + + @Inject(method = "mouseReleased", at = @At("HEAD"), cancellable = true) + private void onMouseReleased(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) { + if (ContainerPreview.isMouseOverLockedTooltip((int) mouseX, (int) mouseY)) { + cir.setReturnValue(true); + } + } + } +} diff --git a/src/main/java/com/lambda/mixin/render/ScreenMixin.java b/src/main/java/com/lambda/mixin/render/ScreenMixin.java index 1780bb8a7..9c4972653 100644 --- a/src/main/java/com/lambda/mixin/render/ScreenMixin.java +++ b/src/main/java/com/lambda/mixin/render/ScreenMixin.java @@ -18,7 +18,11 @@ package com.lambda.mixin.render; import com.lambda.gui.components.QuickSearch; +import com.lambda.module.modules.render.ContainerPreview; import com.lambda.module.modules.render.NoRender; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import org.lwjgl.glfw.GLFW; @@ -42,4 +46,13 @@ private void onKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfo private void injectRenderInGameBackground(DrawContext context, CallbackInfo ci) { if (NoRender.INSTANCE.isEnabled() && NoRender.getNoGuiShadow()) ci.cancel(); } + + @WrapOperation(method = "renderWithTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;render(Lnet/minecraft/client/gui/DrawContext;IIF)V")) + private void wrapRender(Screen instance, DrawContext context, int mouseX, int mouseY, float deltaTicks, Operation original) { + original.call(instance, context, mouseX, mouseY, deltaTicks); + + if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) { + ContainerPreview.renderLockedTooltip(context, MinecraftClient.getInstance().textRenderer); + } + } } diff --git a/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java b/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java index b33fba5c1..1088f5c65 100644 --- a/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java +++ b/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java @@ -17,6 +17,7 @@ package com.lambda.mixin.render; +import com.lambda.module.modules.render.ContainerPreview; import com.lambda.module.modules.render.MapPreview; import net.minecraft.client.gui.tooltip.BundleTooltipComponent; import net.minecraft.client.gui.tooltip.ProfilesTooltipComponent; @@ -32,6 +33,11 @@ public interface TooltipComponentMixin { @Inject(method = "of(Lnet/minecraft/item/tooltip/TooltipData;)Lnet/minecraft/client/gui/tooltip/TooltipComponent;", at = @At("HEAD"), cancellable = true) private static void of(TooltipData tooltipData, CallbackInfoReturnable cir) { + if (ContainerPreview.INSTANCE.isEnabled() && tooltipData instanceof ContainerPreview.ContainerComponent containerComponent) { + cir.setReturnValue(containerComponent); + return; + } + if (MapPreview.INSTANCE.isEnabled()) cir.setReturnValue((switch (tooltipData) { case MapPreview.MapComponent mapComponent -> mapComponent; case BundleTooltipData bundleTooltipData -> new BundleTooltipComponent(bundleTooltipData.contents()); diff --git a/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt b/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt new file mode 100644 index 000000000..04674e1cb --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt @@ -0,0 +1,339 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.render + +import com.lambda.Lambda.mc +import com.lambda.config.settings.complex.Bind +import com.lambda.interaction.material.container.containers.EnderChestContainer +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.KeyCode +import com.lambda.util.item.ItemStackUtils.shulkerBoxContents +import com.lambda.util.item.ItemUtils.shulkerBoxes +import net.minecraft.block.ShulkerBoxBlock +import net.minecraft.client.font.TextRenderer +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.tooltip.TooltipComponent +import net.minecraft.client.render.RenderLayer +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.item.tooltip.TooltipData +import net.minecraft.util.DyeColor +import net.minecraft.util.Identifier +import org.lwjgl.glfw.GLFW + +object ContainerPreview : Module( + name = "ContainerPreview", + description = "Renders shulker box contents visually in tooltips", + tag = ModuleTag.RENDER, +) { + private val lockKey by setting("Lock Key", Bind(KeyCode.LeftShift.code, 0, -1), "Key to lock the tooltip in place for item interaction") + private val useShift by setting("Use Shift", true, "Use shift key to lock tooltip (overrides lock key)") + private val colorTint by setting("Color Tint", true, "Tint the background with the shulker box color") + + private val background = Identifier.ofVanilla("textures/gui/container/shulker_box.png") + + private var lockedStack: ItemStack? = null + private var lockedX: Int = 0 + private var lockedY: Int = 0 + + @JvmStatic + var isRenderingSubTooltip: Boolean = false + private set + + private const val ROWS = 3 + private const val COLS = 9 + private const val SLOT_SIZE = 18 + private const val PADDING = 7 + private const val TITLE_HEIGHT = 14 + + @JvmStatic + val isLocked: Boolean + get() = lockedStack != null + + @JvmStatic + fun isLockKeyPressed(): Boolean { + if (!isEnabled) return false + if (useShift) return Screen.hasShiftDown() + val handle = mc.window.handle + return GLFW.glfwGetKey(handle, lockKey.key) == GLFW.GLFW_PRESS + } + + private fun getTooltipWidth() = PADDING + COLS * SLOT_SIZE + PADDING + private fun getTooltipHeight() = TITLE_HEIGHT + ROWS * SLOT_SIZE + PADDING + + /** + * Check if mouse is over the locked tooltip area (for click blocking) + */ + @JvmStatic + fun isMouseOverLockedTooltip(mouseX: Int, mouseY: Int): Boolean { + if (!isLocked) return false + val width = getTooltipWidth() + val height = getTooltipHeight() + return mouseX >= lockedX && mouseX < lockedX + width && + mouseY >= lockedY && mouseY < lockedY + height + } + + /** + * Calculate text color based on background luminance. + * Returns dark text for light backgrounds and white text for dark backgrounds. + */ + private fun getTextColor(tintColor: Int): Int { + val r = ((tintColor shr 16) and 0xFF) / 255f + val g = ((tintColor shr 8) and 0xFF) / 255f + val b = (tintColor and 0xFF) / 255f + val luminance = 0.299f * r + 0.587f * g + 0.114f * b + return if (luminance > 0.5f) 0x404040 else 0xFFFFFF + } + + @JvmStatic + fun renderShulkerTooltip( + context: DrawContext, + textRenderer: TextRenderer, + component: ContainerComponent, + mouseX: Int, + mouseY: Int + ) { + val width = getTooltipWidth() + val height = getTooltipHeight() + + val lockKeyPressed = isLockKeyPressed() + + if (lockKeyPressed && lockedStack == null) { + lockedStack = component.stack.copy() + lockedX = calculateTooltipX(mouseX, width) + lockedY = calculateTooltipY(mouseY, height) + } else if (!lockKeyPressed && lockedStack != null) { + lockedStack = null + } + + if (isLocked) { + renderLockedTooltipInternal(context, textRenderer) + return + } + + renderTooltipForStack(context, textRenderer, component.stack, calculateTooltipX(mouseX, width), calculateTooltipY(mouseY, height), false) + } + + /** + * Render the locked tooltip - called from mixin when we're locked + */ + @JvmStatic + fun renderLockedTooltip(context: DrawContext, textRenderer: TextRenderer) { + if (!isLockKeyPressed()) { + lockedStack = null + return + } + renderLockedTooltipInternal(context, textRenderer) + } + + private fun renderLockedTooltipInternal(context: DrawContext, textRenderer: TextRenderer) { + val stack = lockedStack ?: return + renderTooltipForStack(context, textRenderer, stack, lockedX, lockedY, true) + } + + private fun renderTooltipForStack(context: DrawContext, textRenderer: TextRenderer, stack: ItemStack, x: Int, y: Int, allowHover: Boolean) { + val contents = getContainerContents(stack) + val width = getTooltipWidth() + val height = getTooltipHeight() + + val matrices = context.matrices + matrices.push() + matrices.translate(0f, 0f, 400f) + + val tintColor = getContainerTintColor(stack) + + drawBackground(context, x, y, width, height, tintColor) + val name = stack.name + val textColor = getTextColor(tintColor) + context.drawText(textRenderer, name, x + PADDING, y + 4, textColor, false) + + val slotsStartX = x + PADDING + val slotsStartY = y + TITLE_HEIGHT + + val actualMouseX = (mc.mouse.x * mc.window.scaledWidth / mc.window.width).toInt() + val actualMouseY = (mc.mouse.y * mc.window.scaledHeight / mc.window.height).toInt() + + var hoveredStack: ItemStack? = null + var hoveredSlotX = 0 + var hoveredSlotY = 0 + + for ((index, item) in contents.withIndex()) { + if (index >= COLS * ROWS) break + + val slotCol = index % COLS + val slotRow = index / COLS + + val slotX = slotsStartX + slotCol * SLOT_SIZE + val slotY = slotsStartY + slotRow * SLOT_SIZE + val itemX = slotX + 1 + val itemY = slotY + 1 + + if (allowHover) { + val isHovered = actualMouseX >= slotX && actualMouseX < slotX + SLOT_SIZE && + actualMouseY >= slotY && actualMouseY < slotY + SLOT_SIZE + + if (isHovered && !item.isEmpty) { + context.fill(itemX, itemY, itemX + 16, itemY + 16, 0x80FFFFFF.toInt()) + hoveredStack = item + hoveredSlotX = actualMouseX + hoveredSlotY = actualMouseY + } + } + + if (!item.isEmpty) { + context.drawItem(item, itemX, itemY) + context.drawStackOverlay(textRenderer, item, itemX, itemY) + } + } + + matrices.pop() + + hoveredStack?.let { stack -> + matrices.push() + matrices.translate(0f, 0f, 500f) + + if (isPreviewableContainer(stack)) { + val nestedWidth = getTooltipWidth() + val nestedHeight = getTooltipHeight() + val nestedX = calculateTooltipX(hoveredSlotX, nestedWidth) + val nestedY = calculateTooltipY(hoveredSlotY, nestedHeight) + renderTooltipForStack(context, textRenderer, stack, nestedX, nestedY, false) + } else { + isRenderingSubTooltip = true + try { + context.drawItemTooltip(textRenderer, stack, hoveredSlotX, hoveredSlotY) + } finally { + isRenderingSubTooltip = false + } + } + matrices.pop() + } + } + + private fun getContainerContents(stack: ItemStack): List { + return when { + isShulkerBox(stack) -> stack.shulkerBoxContents + isEnderChest(stack) -> EnderChestContainer.stacks + else -> emptyList() + } + } + + private fun getContainerTintColor(stack: ItemStack): Int { + if (!colorTint) return 0xFFFFFFFF.toInt() + + return when { + isShulkerBox(stack) -> { + val color = getShulkerColor(stack) + color?.entityColor ?: 0xFFFFFFFF.toInt() + } + isEnderChest(stack) -> 0xFF1E1E2E.toInt() + else -> 0xFFFFFFFF.toInt() + } + } + + private fun calculateTooltipX(mouseX: Int, width: Int): Int { + val screenWidth = mc.window.scaledWidth + var x = mouseX + 12 + if (x + width > screenWidth) { + x = mouseX - width - 12 + } + if (x < 0) x = 0 + return x + } + + private fun calculateTooltipY(mouseY: Int, height: Int): Int { + val screenHeight = mc.window.scaledHeight + var y = mouseY - 12 + if (y + height > screenHeight) { + y = screenHeight - height + } + if (y < 0) y = 0 + return y + } + + private fun drawBackground(context: DrawContext, x: Int, y: Int, width: Int, height: Int, tintColor: Int) { + // Draw the shulker box texture background with tint + // The shulker_box.png texture is 176x166 + // Top part (title area): y=0 to y=17 + // Slot area: y=17 to y=89 (3 rows of 18px each + borders) + // Bottom: y=160 onwards + + context.drawTexture( + RenderLayer::getGuiTextured, + background, + x, y, + 0f, 0f, + width, TITLE_HEIGHT, + 256, 256, + tintColor + ) + + // Middle rows + (0 until ROWS).forEach { row -> + context.drawTexture( + RenderLayer::getGuiTextured, + background, + x, y + TITLE_HEIGHT + row * SLOT_SIZE, + 0f, 17f, + width, SLOT_SIZE, + 256, 256, + tintColor + ) + } + + // Bottom + context.drawTexture( + RenderLayer::getGuiTextured, + background, + x, y + TITLE_HEIGHT + ROWS * SLOT_SIZE, + 0f, 160f, + width, PADDING, + 256, 256, + tintColor + ) + } + + private fun getShulkerColor(stack: ItemStack): DyeColor? { + val item = stack.item + if (item is BlockItem) { + val block = item.block + if (block is ShulkerBoxBlock) { + return block.color + } + } + return null + } + + @JvmStatic + fun isShulkerBox(stack: ItemStack) = stack.item in shulkerBoxes + + @JvmStatic + fun isEnderChest(stack: ItemStack) = stack.item == Items.ENDER_CHEST && EnderChestContainer.stacks.isNotEmpty() + + @JvmStatic + fun isPreviewableContainer(stack: ItemStack) = isShulkerBox(stack) || isEnderChest(stack) + + open class ContainerComponent(val stack: ItemStack) : TooltipData, TooltipComponent { + override fun drawItems(textRenderer: TextRenderer, x: Int, y: Int, width: Int, height: Int, context: DrawContext) {} + override fun getHeight(textRenderer: TextRenderer): Int = 0 + override fun getWidth(textRenderer: TextRenderer): Int = 0 + } +} diff --git a/src/main/resources/lambda.mixins.common.json b/src/main/resources/lambda.mixins.common.json index c865de961..f11bdfb07 100644 --- a/src/main/resources/lambda.mixins.common.json +++ b/src/main/resources/lambda.mixins.common.json @@ -21,6 +21,7 @@ "input.KeyboardMixin", "input.MouseMixin", "items.BarrierBlockMixin", + "items.BlockItemMixin", "items.FilledMapItemMixin", "network.ClientConnectionMixin", "network.ClientLoginNetworkMixin", @@ -46,11 +47,13 @@ "render.ChunkOcclusionDataBuilderMixin", "render.DebugHudMixin", "render.DebugRendererMixin", + "render.DrawContextMixin", "render.ElytraFeatureRendererMixin", "render.EnchantingTableBlockEntityRendererMixin", "render.EntityRendererMixin", "render.FluidRendererMixin", "render.GameRendererMixin", + "render.HandledScreenMixin", "render.GlStateManagerMixin", "render.HeadFeatureRendererMixin", "render.HeldItemRendererMixin",