Skip to content

Commit 57b5027

Browse files
authored
Shulker/Bundle contents preview of the most common item (#286)
1 parent 0547c20 commit 57b5027

4 files changed

Lines changed: 143 additions & 4 deletions

File tree

src/main/java/com/lambda/mixin/render/DrawContextMixin.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@
2222
import net.minecraft.client.MinecraftClient;
2323
import net.minecraft.client.font.TextRenderer;
2424
import net.minecraft.client.gui.DrawContext;
25+
import net.minecraft.client.gui.render.state.GuiRenderState;
2526
import net.minecraft.client.render.MapRenderState;
2627
import net.minecraft.component.DataComponentTypes;
28+
import net.minecraft.entity.LivingEntity;
2729
import net.minecraft.item.FilledMapItem;
2830
import net.minecraft.item.ItemStack;
2931
import net.minecraft.item.Items;
3032
import net.minecraft.item.tooltip.TooltipData;
3133
import net.minecraft.text.Text;
3234
import net.minecraft.util.Identifier;
35+
import net.minecraft.world.World;
3336
import org.jetbrains.annotations.Nullable;
3437
import org.joml.Matrix3x2fStack;
3538
import org.spongepowered.asm.mixin.Final;
@@ -48,6 +51,10 @@ public abstract class DrawContextMixin {
4851
@Shadow
4952
@Final
5053
MinecraftClient client;
54+
@Unique boolean adjustSize = false;
55+
@Shadow
56+
@Final
57+
public GuiRenderState state;
5158

5259
@Unique
5360
private final MapRenderState mapRenderState = new MapRenderState();
@@ -95,4 +102,10 @@ private void onDrawTooltip(TextRenderer textRenderer, List<Text> text, Optional<
95102
ContainerPreview.renderShulkerTooltip((DrawContext)(Object)this, textRenderer, x, y);
96103
}
97104
}
105+
106+
@Inject(method = "drawItem(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/world/World;Lnet/minecraft/item/ItemStack;III)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/state/GuiRenderState;addItem(Lnet/minecraft/client/gui/render/state/ItemGuiElementRenderState;)V", shift = At.Shift.AFTER))
107+
private void onDrawItem(LivingEntity entity, World world, ItemStack stack, int x, int y, int seed, CallbackInfo ci) {
108+
if (!ContainerPreview.INSTANCE.isEnabled()) return;
109+
ContainerPreview.drawOnItem((DrawContext) (Object) this, state, entity, world, stack, x, y, seed);
110+
}
98111
}

src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,57 @@ import com.lambda.interaction.material.container.containers.EnderChestContainer
2323
import com.lambda.module.Module
2424
import com.lambda.module.tag.ModuleTag
2525
import com.lambda.threading.runSafe
26+
import com.lambda.util.Describable
2627
import com.lambda.util.InputUtils.isSatisfied
2728
import com.lambda.util.KeyCode
29+
import com.lambda.util.NamedEnum
30+
import com.lambda.util.item.ItemStackUtils.bundleContents
2831
import com.lambda.util.item.ItemStackUtils.shulkerBoxContents
32+
import com.lambda.util.item.ItemUtils.bundles
2933
import com.lambda.util.item.ItemUtils.shulkerBoxes
34+
import com.lambda.util.text.buildText
35+
import com.lambda.util.text.literal
3036
import net.minecraft.block.ShulkerBoxBlock
3137
import net.minecraft.client.font.TextRenderer
32-
import net.minecraft.client.gui.DrawContext
33-
import net.minecraft.client.gui.tooltip.TooltipComponent
3438
import net.minecraft.client.gl.RenderPipelines
39+
import net.minecraft.client.gui.DrawContext
40+
import net.minecraft.client.gui.render.state.GuiRenderState
41+
import net.minecraft.client.gui.render.state.ItemGuiElementRenderState
42+
import net.minecraft.client.gui.render.state.TextGuiElementRenderState
3543
import net.minecraft.client.gui.screen.ingame.HandledScreen
44+
import net.minecraft.client.gui.tooltip.TooltipComponent
45+
import net.minecraft.client.render.item.KeyedItemRenderState
46+
import net.minecraft.entity.LivingEntity
3647
import net.minecraft.item.BlockItem
48+
import net.minecraft.item.ItemDisplayContext
3749
import net.minecraft.item.ItemStack
3850
import net.minecraft.item.Items
3951
import net.minecraft.item.tooltip.TooltipData
4052
import net.minecraft.screen.slot.Slot
4153
import net.minecraft.util.Colors
4254
import net.minecraft.util.DyeColor
4355
import net.minecraft.util.Identifier
56+
import net.minecraft.world.World
57+
import org.joml.Matrix3x2f
58+
import kotlin.math.max
59+
4460

4561
object ContainerPreview : Module(
4662
name = "ContainerPreview",
4763
description = "Renders shulker box contents visually in tooltips",
4864
tag = ModuleTag.RENDER,
4965
) {
50-
private val lockKey by setting("Lock Key", Bind(KeyCode.LeftShift.code, 0, -1), "Key to lock the tooltip in place for item interaction")
51-
private val colorTint by setting("Color Tint", true, "Tint the background with the shulker box color")
66+
private val lockKey by setting("Lock Key", Bind(KeyCode.LeftShift.code, 0, -1), "Key to lock the tooltip in place for item interaction").group(Group.ContainerTooltip)
67+
private val colorTint by setting("Color Tint", true, "Tint the background with the shulker box color").group(Group.ContainerTooltip)
68+
69+
private val contentPreview by setting("Content Preview", true, "Show a preview of the most common item in a container on the container item in inventories").group(Group.ContentPreview)
70+
private val previewItemScale by setting("Item Scale", 13f, 1f..32f, 0.1f, "Scale of the item icons on a container item") { contentPreview }.group(Group.ContentPreview)
71+
private val previewItemXOffset by setting("Item X Offset", 0f, -32f..32f, 0.1f, "X offset of the item icons on a container item") { contentPreview }.group(Group.ContentPreview)
72+
private val previewItemYOffset by setting("Item Y Offset", 0f, -32f..32f, 0.1f, "Y offset of the item icons on a container item") { contentPreview }.group(Group.ContentPreview)
73+
private val previewItemWeightedCount by setting("Weighted Count", true, description = "Count items for preview in containers relative to max stack size") { contentPreview }.group(Group.ContentPreview)
74+
.onValueChange { _, _ ->
75+
containerCache.clear()
76+
}
5277

5378
private val background = Identifier.ofVanilla("textures/gui/container/shulker_box.png")
5479

@@ -65,6 +90,13 @@ object ContainerPreview : Module(
6590
var isRenderingSubTooltip: Boolean = false
6691
private set
6792

93+
// Cache for container contents summary // Cache size is limited to 200 entries
94+
val containerCache = object : LinkedHashMap<Int, ContainerPreviewInfo>(16, 0.75f, true) {
95+
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, ContainerPreviewInfo>): Boolean {
96+
return size > 200
97+
}
98+
}
99+
68100
private const val ROWS = 3
69101
private const val COLS = 9
70102
private const val SLOT_SIZE = 18
@@ -325,6 +357,28 @@ object ContainerPreview : Module(
325357
return null
326358
}
327359

360+
private fun getPreviewItemForContainer(container: ItemStack): ContainerPreviewInfo {
361+
val hash = container.hashCode()
362+
363+
return containerCache.computeIfAbsent(hash) {
364+
val contents = container.shulkerBoxContents + container.bundleContents
365+
if (contents.isEmpty()) return@computeIfAbsent ContainerPreviewInfo(null, false)
366+
367+
val group = contents.filter { stack -> stack.item != Items.AIR }
368+
.groupBy { stack -> stack.item }
369+
.map { (item, stacks) ->
370+
val stackWeight = if (previewItemWeightedCount) 64f / item.maxCount else 1f
371+
stacks.first() to (stacks.sumOf { it.count } * stackWeight)
372+
}
373+
val unique = group.size
374+
val mostCommon = group.maxByOrNull { (_, weightedCount) -> weightedCount }?.let { (stack, count) ->
375+
stack.copyWithCount(max(1, count.toInt().coerceAtMost(stack.maxCount)))
376+
}
377+
378+
ContainerPreviewInfo(mostCommon, unique > 1)
379+
}
380+
}
381+
328382
@JvmStatic
329383
fun isShulkerBox(stack: ItemStack) = stack.item in shulkerBoxes
330384

@@ -334,9 +388,57 @@ object ContainerPreview : Module(
334388
@JvmStatic
335389
fun isPreviewableContainer(stack: ItemStack) = isShulkerBox(stack) || isEnderChest(stack)
336390

391+
@JvmStatic
392+
fun isBundle(stack: ItemStack) = stack.item in bundles
393+
394+
@JvmStatic
395+
fun drawOnItem(drawContext: DrawContext, state: GuiRenderState, entity: LivingEntity?, world: World?, stack: ItemStack, x: Int, y: Int, seed: Int) {
396+
if (!contentPreview) return
397+
if (!isShulkerBox(stack) && !isBundle(stack)) return
398+
val preview = getPreviewItemForContainer(stack)
399+
if (preview.stack == null) return
400+
401+
// Apply scaling
402+
val scale = previewItemScale / 16.0f
403+
val itemMatrix = Matrix3x2f(drawContext.matrices)
404+
405+
// Required to center the icon correctly, due to how the item gets centered by the renderer
406+
val shift = 8 * (1 - scale) // 0 at scale 1.0, 8 at scale 0.0
407+
408+
val newScreenX = ((x + previewItemXOffset + shift) / scale).toInt()
409+
val newScreenY = ((y + previewItemYOffset + shift) / scale).toInt()
410+
411+
itemMatrix.scale(scale, scale)
412+
413+
val keyedItemRenderState = KeyedItemRenderState()
414+
mc.itemModelManager.clearAndUpdate(keyedItemRenderState, preview.stack, ItemDisplayContext.GUI, world, entity, seed)
415+
416+
state.addItem(
417+
ItemGuiElementRenderState(
418+
preview.stack.item.name.toString(), itemMatrix, keyedItemRenderState, newScreenX, newScreenY, drawContext.scissorStack.peekLast()
419+
)
420+
)
421+
if (preview.hasMore) {
422+
state.addText(
423+
TextGuiElementRenderState(
424+
mc.textRenderer, buildText {
425+
literal("+")
426+
}.asOrderedText(), itemMatrix, newScreenX + 14, newScreenY - 2, -1, Integer.MIN_VALUE, true, false, drawContext.scissorStack.peekLast()
427+
)
428+
)
429+
}
430+
}
431+
432+
enum class Group(override val displayName: String, override val description: String) : NamedEnum, Describable {
433+
ContentPreview("Preview", "Settings related to the item preview rendered on container items in inventories"),
434+
ContainerTooltip("Container", "Settings related to container tooltip previews")
435+
}
436+
337437
open class ContainerComponent(val stack: ItemStack) : TooltipData, TooltipComponent {
338438
override fun drawItems(textRenderer: TextRenderer, x: Int, y: Int, width: Int, height: Int, context: DrawContext) {}
339439
override fun getHeight(textRenderer: TextRenderer): Int = 0
340440
override fun getWidth(textRenderer: TextRenderer): Int = 0
341441
}
442+
443+
data class ContainerPreviewInfo(val stack: ItemStack?, val hasMore: Boolean)
342444
}

src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ object ItemStackUtils {
115115
stack.components.get(DataComponentTypes.CONTAINER)?.stream()?.toList() ?: emptyList()
116116
}
117117

118+
val ItemStack.bundleContents: List<ItemStack> by cacheable { stack ->
119+
stack.components.get(DataComponentTypes.BUNDLE_CONTENTS)?.stream()?.toList() ?: emptyList()
120+
}
121+
118122
/**
119123
* Checks if the given item stacks are equal, including the item count and NBT.
120124
*/

src/main/kotlin/com/lambda/util/item/ItemUtils.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,26 @@ object ItemUtils {
9797
Items.BLACK_SHULKER_BOX,
9898
)
9999

100+
val bundles = setOf(
101+
Items.BUNDLE,
102+
Items.WHITE_BUNDLE,
103+
Items.ORANGE_BUNDLE,
104+
Items.MAGENTA_BUNDLE,
105+
Items.LIGHT_BLUE_BUNDLE,
106+
Items.YELLOW_BUNDLE,
107+
Items.LIME_BUNDLE,
108+
Items.PINK_BUNDLE,
109+
Items.GRAY_BUNDLE,
110+
Items.LIGHT_GRAY_BUNDLE,
111+
Items.CYAN_BUNDLE,
112+
Items.PURPLE_BUNDLE,
113+
Items.BLUE_BUNDLE,
114+
Items.BROWN_BUNDLE,
115+
Items.GREEN_BUNDLE,
116+
Items.RED_BUNDLE,
117+
Items.BLACK_BUNDLE
118+
)
119+
100120
val chests = setOf(
101121
Items.CHEST,
102122
Items.TRAPPED_CHEST,

0 commit comments

Comments
 (0)