diff --git a/common/src/main/resources/modernfix.accesswidener b/common/src/main/resources/modernfix.accesswidener index 54f5368c..57322acc 100644 --- a/common/src/main/resources/modernfix.accesswidener +++ b/common/src/main/resources/modernfix.accesswidener @@ -67,5 +67,6 @@ accessible field net/minecraft/server/packs/resources/ProfiledReloadInstance$Sta accessible class net/minecraft/world/item/crafting/Ingredient$Value accessible field net/minecraft/world/item/crafting/Ingredient$TagValue tag Lnet/minecraft/tags/TagKey; +accessible field net/minecraft/world/item/crafting/Ingredient$ItemValue item Lnet/minecraft/world/item/ItemStack; accessible class net/minecraft/world/item/crafting/Ingredient$ItemValue accessible class net/minecraft/client/searchtree/SearchRegistry$TreeEntry \ No newline at end of file diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/ForgeHooksMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/ForgeHooksMixin.java new file mode 100644 index 00000000..a5cd1ca3 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/ForgeHooksMixin.java @@ -0,0 +1,23 @@ +package org.embeddedt.modernfix.forge.mixin.perf.faster_ingredients; + +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.ForgeHooks; +import org.embeddedt.modernfix.forge.recipe.ExtendedIngredient; +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(value = ForgeHooks.class, priority = 900) +public class ForgeHooksMixin { + /** + * @author embeddedt + * @reason Exploding the stack list is entirely unnecessary to compute this + */ + @Inject(method = "hasNoElements", at = @At("HEAD"), cancellable = true, remap = false) + private static void modernfix$fastHasNoElements(Ingredient ingredient, CallbackInfoReturnable cir) { + if (ingredient.isVanilla()) { + cir.setReturnValue(((ExtendedIngredient)ingredient).mfix$hasNoElements()); + } + } +} diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java new file mode 100644 index 00000000..5fa2ab02 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/mixin/perf/faster_ingredients/IngredientMixin.java @@ -0,0 +1,134 @@ +package org.embeddedt.modernfix.forge.mixin.perf.faster_ingredients; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntComparators; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.embeddedt.modernfix.forge.recipe.ExtendedIngredient; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; + +@Mixin(value = Ingredient.class, priority = 700) +public abstract class IngredientMixin implements ExtendedIngredient { + @Shadow + public abstract boolean isVanilla(); + + @Shadow @Final + private Ingredient.Value[] values; + + @Shadow private @Nullable IntList stackingIds; + + @Shadow @Nullable private ItemStack[] itemStacks; + + /** + * @author embeddedt + * @reason tag ingredients can be tested without iterating over all items + */ + @Inject(method = "test(Lnet/minecraft/world/item/ItemStack;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/crafting/Ingredient;getItems()[Lnet/minecraft/world/item/ItemStack;"), cancellable = true) + private void modernfix$fasterTagIngredientTest(ItemStack stack, CallbackInfoReturnable cir) { + if (this.isVanilla() && this.values.length == 1 && this.values[0] instanceof Ingredient.TagValue tagValue) { + cir.setReturnValue(stack.getItemHolder().is(tagValue.tag)); + } + } + + @Override + public boolean mfix$hasNoElements() { + return !this.containsItems(); + } + + @Unique + private boolean isEmptyTagStack(ItemStack item) { + return item.getItem() == net.minecraft.world.item.Items.BARRIER && item.getHoverName() instanceof net.minecraft.network.chat.MutableComponent hoverName && hoverName.getString().startsWith("Empty Tag: "); + } + + @Unique + private boolean containsItems() { + for (Ingredient.Value value : this.values) { + if (value instanceof Ingredient.ItemValue) { + return true; + } else if (value instanceof Ingredient.TagValue tagValue) { + var holderSetOpt = BuiltInRegistries.ITEM.getTag(tagValue.tag); + if (holderSetOpt.isPresent() && holderSetOpt.get().size() > 0) { + return true; + } + } else { + var items = value.getItems(); + if (items.isEmpty() || isEmptyTagStack(items.iterator().next())) { + // Doesn't have items + continue; + } + return true; + } + } + return false; + } + + /** + * @author embeddedt + * @reason tag ingredients can be converted to stacking IDs without expanding into stacks, since stacking only + * goes by item ID + */ + @Inject(method = "getStackingIds", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/crafting/Ingredient;getItems()[Lnet/minecraft/world/item/ItemStack;"), cancellable = true) + private void modernfix$fasterTagIngredientStacking(CallbackInfoReturnable cir) { + if (this.isVanilla() && this.values.length == 1 && this.values[0] instanceof Ingredient.TagValue tagValue) { + var tag = BuiltInRegistries.ITEM.getTag(tagValue.tag); + if (!tag.isPresent() || tag.get().size() == 0) { + return; + } + var list = new IntArrayList(tag.get().stream().mapToInt(h -> BuiltInRegistries.ITEM.getId(h.value())).toArray()); + list.sort(IntComparators.NATURAL_COMPARATOR); + this.stackingIds = list; + cir.setReturnValue(list); + } + } + + /** + * @author embeddedt + * @reason remove caching of the item stacks, it won't deduplicate anything with tags (since each Ingredient + * instance would make new item stacks anyway, and storing them permanently takes up a lot of memory). + * We implement an optimized version of some functions that avoids needing to call this entirely. + */ + @Overwrite + public ItemStack[] getItems() { + // For compatibility if mods explicitly force a set of item stacks to be used + if (this.itemStacks != null) { + return this.itemStacks; + } + // Fast path for case with one item + if (this.values.length == 1) { + if (this.values[0] instanceof Ingredient.ItemValue itemValue) { + return new ItemStack[] { itemValue.item }; + } else if (this.values[0] instanceof Ingredient.TagValue tagValue) { + var tag = BuiltInRegistries.ITEM.getTag(tagValue.tag); + if (tag.isPresent() && tag.get().size() > 0) { + var holderSet = tag.get(); + ItemStack[] result = new ItemStack[holderSet.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = new ItemStack(holderSet.get(i)); + } + return result; + } + } + } + ArrayList itemList = new ArrayList<>(2); + for (var value : this.values) { + var collection = value.getItems(); + itemList.ensureCapacity(collection.size() + itemList.size()); + for (var item : collection) { + itemList.add(item); + } + } + return itemList.toArray(ItemStack[]::new); + } +} diff --git a/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java b/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java new file mode 100644 index 00000000..db7014b4 --- /dev/null +++ b/forge/src/main/java/org/embeddedt/modernfix/forge/recipe/ExtendedIngredient.java @@ -0,0 +1,5 @@ +package org.embeddedt.modernfix.forge.recipe; + +public interface ExtendedIngredient { + boolean mfix$hasNoElements(); +}