From da89f5b6c6198e750d83c77fdb887e9151c3f8ac Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Sun, 9 Aug 2026 14:12:24 +0530 Subject: [PATCH 1/2] Fix Forgified Fabric API creative inventory crash Signed-off-by: Arnab Nandy --- CHANGELOG.md | 3 +- .../mixin/dev/ShapeMapHashContractMixin.java | 62 +++++++++++++++++++ .../mixin/item/ItemStackLinkedSetMixin.java | 19 ++++++ .../common/mixin/item/ItemStackMixin.java | 4 +- src/main/resources/clutternomore.mixins.json | 2 + 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java create mode 100644 src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6b696..3586f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,5 @@ - Fixed compatibility issue with Enchancement caused by mixins not chaining. - Fixed `/reload` breaking some functionality. - Fixed lang overrides not being respected in some cases. -- Added some config options to revert the loot table and recipe changes by the mod. \ No newline at end of file +- Added some config options to revert the loot table and recipe changes by the mod. +- Fixed an intermittent creative inventory crash with Forgified Fabric API on 1.20.1. diff --git a/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java b/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java new file mode 100644 index 0000000..0463625 --- /dev/null +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java @@ -0,0 +1,62 @@ +package dev.tazer.clutternomore.common.mixin.dev; + +import dev.tazer.clutternomore.common.mixin.annotation.IfDevEnvironment; +import dev.tazer.clutternomore.common.shape_map.ShapeMap; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackLinkedSet; +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; +//? if forge { +/*import net.minecraft.nbt.CompoundTag; +import net.minecraftforge.common.util.MutableHashedLinkedMap; +*///?} + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +@IfDevEnvironment +@Mixin(ShapeMap.class) +public class ShapeMapHashContractMixin { + @Inject(method = "setMappings", at = @At("RETURN")) + private static void cnm$verifyItemStackHashContract(List mappings, boolean detailedLogs, CallbackInfo ci) { + Iterator> shapes = ShapeMap.inverseView().entrySet().iterator(); + if (!shapes.hasNext()) return; + + Map.Entry mapping = shapes.next(); + ItemStack shape = new ItemStack(mapping.getKey()); + ItemStack parent = new ItemStack(mapping.getValue()); + + if (!ItemStackLinkedSet.TYPE_AND_TAG.equals(shape, parent)) { + throw new IllegalStateException("Shape and parent stacks must compare equally"); + } + if (ItemStackLinkedSet.TYPE_AND_TAG.hashCode(shape) != ItemStackLinkedSet.TYPE_AND_TAG.hashCode(parent)) { + throw new IllegalStateException("Equal shape and parent stacks must have equal hashes"); + } + + //? if forge { + /* + ItemStack taggedShape = shape.copy(); + CompoundTag tag = new CompoundTag(); + tag.putBoolean("clutternomore_hash_contract_test", true); + taggedShape.setTag(tag); + if (ItemStackLinkedSet.TYPE_AND_TAG.equals(taggedShape, parent)) { + throw new IllegalStateException("Shape stack data must participate in equality"); + } + + MutableHashedLinkedMap entries = new MutableHashedLinkedMap<>(ItemStackLinkedSet.TYPE_AND_TAG); + entries.put(shape, 0); + entries.put(parent, 1); + for (Iterator> iterator = entries.iterator(); iterator.hasNext(); ) { + iterator.next(); + iterator.remove(); + } + if (!entries.isEmpty()) { + throw new IllegalStateException("Shape stack map must be empty after iterator removal"); + } + *///?} + } +} diff --git a/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java new file mode 100644 index 0000000..a9f2cd7 --- /dev/null +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java @@ -0,0 +1,19 @@ +package dev.tazer.clutternomore.common.mixin.item; + +import dev.tazer.clutternomore.common.shape_map.ShapeMap; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackLinkedSet; +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(targets = "net.minecraft.world.item.ItemStackLinkedSet$1") +public class ItemStackLinkedSetMixin { + @Inject(method = "hashCode(Lnet/minecraft/world/item/ItemStack;)I", at = @At("HEAD"), cancellable = true) + private void cnm$hashCode(ItemStack stack, CallbackInfoReturnable cir) { + if (stack != null && ShapeMap.isShape(stack)) { + cir.setReturnValue(ItemStackLinkedSet.TYPE_AND_TAG.hashCode(ShapeMap.transferStack(stack, 0))); + } + } +} diff --git a/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackMixin.java b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackMixin.java index b22912a..587daf5 100644 --- a/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackMixin.java +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackMixin.java @@ -13,7 +13,9 @@ public class ItemStackMixin { //~ if >1.20.1 'isSameItemSameTags' -> 'isSameItemSameComponents' { @Inject(method = "isSameItemSameComponents", at = @At("RETURN"), cancellable = true) private static void cnm$isSameItemSameComponents(ItemStack stack, ItemStack other, CallbackInfoReturnable cir) { - if (!cir.getReturnValue()) cir.setReturnValue(ShapeMap.inSameShapeSet(stack.getItem(), other.getItem())); + if (!cir.getReturnValue() && ShapeMap.inSameShapeSet(stack.getItem(), other.getItem())) { + cir.setReturnValue(ItemStack.isSameItemSameComponents(ShapeMap.transferStack(stack, 0), ShapeMap.transferStack(other, 0))); + } } //~} } diff --git a/src/main/resources/clutternomore.mixins.json b/src/main/resources/clutternomore.mixins.json index 746dac4..79a417d 100644 --- a/src/main/resources/clutternomore.mixins.json +++ b/src/main/resources/clutternomore.mixins.json @@ -20,9 +20,11 @@ "creative.DefaultStackEntryCollectorMixin", "creative.FabricItemGroupEntriesMixin", "creative.ItemDisplayBuilderMixin", + "dev.ShapeMapHashContractMixin", "dev.ShapedRecipeBuilderMixin", "item.ItemMixin", "item.ItemStackMixin", + "item.ItemStackLinkedSetMixin", "pack.MinecraftReloadMixin", "pack.PackRepositoryMixin", "recipe.IngredientMixin", From 64b6ddd4085253e8087aa714c18bdf4b61ba7af1 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 2 Sep 2026 01:11:35 +0530 Subject: [PATCH 2/2] Add before-and-after hash contract regression coverage Signed-off-by: Arnab Nandy --- .../mixin/dev/ShapeMapHashContractMixin.java | 70 +++++++++++++++++-- .../mixin/item/ItemStackLinkedSetMixin.java | 11 +++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java b/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java index 0463625..c2f9114 100644 --- a/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java @@ -1,10 +1,14 @@ +//? if forge && =1.20.1 { package dev.tazer.clutternomore.common.mixin.dev; import dev.tazer.clutternomore.common.mixin.annotation.IfDevEnvironment; +import dev.tazer.clutternomore.ClutterNoMore; import dev.tazer.clutternomore.common.shape_map.ShapeMap; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStackLinkedSet; +import it.unimi.dsi.fastutil.Hash; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -14,14 +18,12 @@ import net.minecraftforge.common.util.MutableHashedLinkedMap; *///?} -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; @IfDevEnvironment -@Mixin(ShapeMap.class) +@Mixin(value = ShapeMap.class, remap = false) public class ShapeMapHashContractMixin { - @Inject(method = "setMappings", at = @At("RETURN")) + @Inject(method = "setMappings", at = @At("RETURN"), remap = false) private static void cnm$verifyItemStackHashContract(List mappings, boolean detailedLogs, CallbackInfo ci) { Iterator> shapes = ShapeMap.inverseView().entrySet().iterator(); if (!shapes.hasNext()) return; @@ -30,6 +32,14 @@ public class ShapeMapHashContractMixin { ItemStack shape = new ItemStack(mapping.getKey()); ItemStack parent = new ItemStack(mapping.getValue()); + if (!ItemStack.isSameItemSameTags(shape, parent)) { + throw new IllegalStateException("Before/after regression setup requires shape and parent stacks to compare equally"); + } + if (cnm$vanillaHash(shape) == cnm$vanillaHash(parent)) { + throw new IllegalStateException("Before regression was not reproduced: equal shape and parent stacks unexpectedly have equal vanilla hashes"); + } + cnm$verifyBrokenStrategyReproducesIteratorCrash(); + if (!ItemStackLinkedSet.TYPE_AND_TAG.equals(shape, parent)) { throw new IllegalStateException("Shape and parent stacks must compare equally"); } @@ -57,6 +67,56 @@ public class ShapeMapHashContractMixin { if (!entries.isEmpty()) { throw new IllegalStateException("Shape stack map must be empty after iterator removal"); } + ClutterNoMore.LOGGER.info("[HashContractRegression] BEFORE reproduced: equal shape/parent stacks had different vanilla hashes; AFTER verified: normalized hashes match and FFAPI iterator removal completed"); *///?} } + + private static int cnm$vanillaHash(ItemStack stack) { + CompoundTag tag = stack.getTag(); + return 31 * (31 + stack.getItem().hashCode()) + (tag == null ? 0 : tag.hashCode()); + } + + private static void cnm$verifyBrokenStrategyReproducesIteratorCrash() { + Hash.Strategy brokenStrategy = new Hash.Strategy<>() { + @Override + public int hashCode(ItemStack stack) { + return stack == null ? 0 : cnm$vanillaHash(stack); + } + + @Override + public boolean equals(ItemStack first, ItemStack second) { + return ItemStackLinkedSet.TYPE_AND_TAG.equals(first, second); + } + }; + + List stacks = new ArrayList<>(); + BuiltInRegistries.ITEM.forEach(item -> stacks.add(new ItemStack(item))); + for (int offset = 0; offset < Math.min(stacks.size(), 128); offset++) { + MutableHashedLinkedMap entries = new MutableHashedLinkedMap<>(brokenStrategy); + for (int index = 0; index < stacks.size(); index++) { + entries.put(stacks.get((index + offset) % stacks.size()), index); + } + try { + for (Iterator> iterator = entries.iterator(); iterator.hasNext(); ) { + iterator.next(); + iterator.remove(); + } + } catch (ConcurrentModificationException expected) { + return; + } + } + throw new IllegalStateException("Before regression did not reproduce MutableHashedLinkedMap iterator removal crash"); + } } +//?} else { +/*package dev.tazer.clutternomore.common.mixin.dev; + +import dev.tazer.clutternomore.common.mixin.annotation.IfDevEnvironment; +import dev.tazer.clutternomore.common.shape_map.ShapeMap; +import org.spongepowered.asm.mixin.Mixin; + +@IfDevEnvironment +@Mixin(value = ShapeMap.class, remap = false) +public class ShapeMapHashContractMixin { +} +*///?} diff --git a/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java index a9f2cd7..6925ea2 100644 --- a/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java @@ -1,3 +1,4 @@ +//? if forge && =1.20.1 { package dev.tazer.clutternomore.common.mixin.item; import dev.tazer.clutternomore.common.shape_map.ShapeMap; @@ -17,3 +18,13 @@ public class ItemStackLinkedSetMixin { } } } +//?} else { +/*package dev.tazer.clutternomore.common.mixin.item; + +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(ItemStack.class) +public class ItemStackLinkedSetMixin { +} +*///?}