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..c2f9114 --- /dev/null +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/dev/ShapeMapHashContractMixin.java @@ -0,0 +1,122 @@ +//? 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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//? if forge { +/*import net.minecraft.nbt.CompoundTag; +import net.minecraftforge.common.util.MutableHashedLinkedMap; +*///?} + +import java.util.*; + +@IfDevEnvironment +@Mixin(value = ShapeMap.class, remap = false) +public class ShapeMapHashContractMixin { + @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; + + Map.Entry mapping = shapes.next(); + 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"); + } + 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"); + } + 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 new file mode 100644 index 0000000..6925ea2 --- /dev/null +++ b/src/main/java/dev/tazer/clutternomore/common/mixin/item/ItemStackLinkedSetMixin.java @@ -0,0 +1,30 @@ +//? if forge && =1.20.1 { +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))); + } + } +} +//?} 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 { +} +*///?} 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",