Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- 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.
Original file line number Diff line number Diff line change
@@ -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<ShapeMap.Mapping> mappings, boolean detailedLogs, CallbackInfo ci) {
Iterator<Map.Entry<Item, Item>> shapes = ShapeMap.inverseView().entrySet().iterator();
if (!shapes.hasNext()) return;

Map.Entry<Item, Item> 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<ItemStack, Integer> entries = new MutableHashedLinkedMap<>(ItemStackLinkedSet.TYPE_AND_TAG);
entries.put(shape, 0);
entries.put(parent, 1);
for (Iterator<Map.Entry<ItemStack, Integer>> 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<ItemStack> 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<ItemStack> stacks = new ArrayList<>();
BuiltInRegistries.ITEM.forEach(item -> stacks.add(new ItemStack(item)));
for (int offset = 0; offset < Math.min(stacks.size(), 128); offset++) {
MutableHashedLinkedMap<ItemStack, Integer> entries = new MutableHashedLinkedMap<>(brokenStrategy);
for (int index = 0; index < stacks.size(); index++) {
entries.put(stacks.get((index + offset) % stacks.size()), index);
}
try {
for (Iterator<Map.Entry<ItemStack, Integer>> 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 {
}
*///?}
Original file line number Diff line number Diff line change
@@ -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<Integer> 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 {
}
*///?}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> 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)));
}
}
//~}
}
2 changes: 2 additions & 0 deletions src/main/resources/clutternomore.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down