Skip to content

Commit 9f2228c

Browse files
committed
refactor: improve NBT comparison logic and enhance armor slot mixin functionality
1 parent 3b4bb67 commit 9f2228c

3 files changed

Lines changed: 67 additions & 10 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
.kotlin/
44
.env
55
compile_config.json
6-
clear_workflows.py
6+
scripts/*.py
77

88
# gradle
99

src/main/kotlin/stackabletools/StackableToolsUtils.kt

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,61 @@ import net.minecraft.registry.Registries
1212
import java.util.concurrent.ConcurrentHashMap
1313
import stackabletools.config.ConfigManager
1414
import stackabletools.config.StackingCategory
15+
import net.minecraft.nbt.NbtCompound
16+
import net.minecraft.item.Items
1517

1618
object StackableToolsUtils {
1719

1820
private val stackableCache = ConcurrentHashMap<String, Boolean>()
1921
private var lastConfigHash = 0
2022

23+
/**
24+
* Compares two ItemStack NBT tags, ignoring cosmetic differences like custom names.
25+
* Enchantments are compared as sets (order-insensitive).
26+
*/
27+
private fun areNBTsEquivalent(nbt1: NbtCompound?, nbt2: NbtCompound?): Boolean {
28+
// Both null = equivalent
29+
if (nbt1 == null && nbt2 == null) return true
30+
// One null, one not = not equivalent
31+
if (nbt1 == null || nbt2 == null) return false
32+
33+
// Extract enchantment lists
34+
val enc1 = nbt1.getList("Enchantments", 10) // 10 = NbtElement.COMPOUND_TYPE
35+
val enc2 = nbt2.getList("Enchantments", 10)
36+
37+
// Compare enchantments as sets (order-insensitive)
38+
if (enc1.size != enc2.size) return false
39+
40+
val enc1Set = mutableSetOf<String>()
41+
for (i in 0 until enc1.size) {
42+
enc1Set.add(enc1.getCompound(i).toString())
43+
}
44+
45+
val enc2Set = mutableSetOf<String>()
46+
for (i in 0 until enc2.size) {
47+
enc2Set.add(enc2.getCompound(i).toString())
48+
}
49+
50+
if (enc1Set != enc2Set) return false
51+
52+
// Check that there are no other conflicting NBT tags (ignore display/cosmetic ones)
53+
val cosmetics = setOf("display", "HideFlags", "CustomModelData", "Damage")
54+
55+
val relevantKeys1 = nbt1.keys.filter { it !in cosmetics }
56+
val relevantKeys2 = nbt2.keys.filter { it !in cosmetics }
57+
58+
if (relevantKeys1.toSet() != relevantKeys2.toSet()) return false
59+
60+
// Compare non-cosmetic keys
61+
for (key in relevantKeys1) {
62+
val v1 = nbt1.get(key)?.toString()
63+
val v2 = nbt2.get(key)?.toString()
64+
if (v1 != v2) return false
65+
}
66+
67+
return true
68+
}
69+
2170
/**
2271
* Checks if two item stacks can be merged (same item, same durability, same NBT).
2372
* @param a First item stack.
@@ -36,9 +85,9 @@ object StackableToolsUtils {
3685
// For other items, check exact durability
3786
if (a.damage != b.damage) return false
3887

39-
// CRUCIAL RULE: Check that enchantments (NBT) are STRICTLY identical
40-
// ItemStack.canCombine already checks for NBT equality (enchantments, custom names, etc.)
41-
if (!ItemStack.canCombine(a, b)) return false
88+
// Compare NBT tags: enchantments must match (order-insensitive),
89+
// but ignore cosmetic differences like custom names
90+
if (!areNBTsEquivalent(a.nbt, b.nbt)) return false
4291

4392
return true
4493
}

src/main/kotlin/stackabletools/mixin/ArmorSlotMixin.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,32 @@ package stackabletools.mixin
22

33
import net.minecraft.item.ItemStack
44
import net.minecraft.screen.slot.Slot
5+
import net.minecraft.screen.PlayerScreenHandler
56
import org.spongepowered.asm.mixin.Mixin
67
import org.spongepowered.asm.mixin.injection.At
78
import org.spongepowered.asm.mixin.injection.Inject
89
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
910

1011
/**
11-
* Mixin specifically targeting armor slots to ensure they never accept more than 1 item,
12+
* Mixin targeting armor slots to ensure they never accept more than 1 item,
1213
* even if our mod makes the item stackable in the general inventory.
1314
*/
14-
@Mixin(targets = ["net.minecraft.screen.PlayerScreenHandler$1"])
15-
abstract class ArmorSlotMixin : Slot(null, 0, 0, 0) {
15+
@Mixin(Slot::class)
16+
abstract class ArmorSlotMixin {
1617

1718
/**
18-
* Forces the maximum item count for the armor slots in PlayerScreenHandler to be 1.
19+
* Forces the maximum item count for armor slots to be 1.
1920
* This prevents stacking items like Elytras or Armor pieces in the equipment slots.
2021
*/
21-
@Inject(method = ["getMaxItemCount(Lnet/minecraft/item/ItemStack;)I"], at = [At("HEAD")], cancellable = true)
22+
@Inject(method = ["getMaxItemCount"], at = [At("HEAD")], cancellable = true)
2223
private fun onGetMaxItemCount(stack: ItemStack, cir: CallbackInfoReturnable<Int>) {
23-
cir.returnValue = 1
24+
val slot = this as Slot
25+
26+
// Check if this is an armor slot (PlayerScreenHandler inner classes target armor/offhand)
27+
// Armor slots are typically indices 5-8 in PlayerScreenHandler
28+
if (slot.javaClass.name.contains("PlayerScreenHandler") &&
29+
(slot.index in 5..8 || slot.javaClass.simpleName in listOf("OffhandSlot", "ArmorSlot"))) {
30+
cir.returnValue = 1
31+
}
2432
}
2533
}

0 commit comments

Comments
 (0)