package net.minecraft.world.item.crafting; import com.google.common.collect.Lists; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntComparators; import it.unimi.dsi.fastutil.ints.IntList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Predicate; import java.util.stream.Stream; import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.tags.TagKey; import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ItemLike; import org.jetbrains.annotations.Nullable; public final class Ingredient implements Predicate { public static final Ingredient EMPTY = new Ingredient(Stream.empty()); public static final StreamCodec CONTENTS_STREAM_CODEC = ItemStack.LIST_STREAM_CODEC .map(list -> fromValues(list.stream().map(Ingredient.ItemValue::new)), ingredient -> Arrays.asList(ingredient.getItems())); private final Ingredient.Value[] values; @Nullable private ItemStack[] itemStacks; @Nullable private IntList stackingIds; public static final Codec CODEC = codec(true); public static final Codec CODEC_NONEMPTY = codec(false); private Ingredient(Stream values) { this.values = (Ingredient.Value[])values.toArray(Ingredient.Value[]::new); } private Ingredient(Ingredient.Value[] values) { this.values = values; } public ItemStack[] getItems() { if (this.itemStacks == null) { this.itemStacks = (ItemStack[])Arrays.stream(this.values).flatMap(value -> value.getItems().stream()).distinct().toArray(ItemStack[]::new); } return this.itemStacks; } public boolean test(@Nullable ItemStack stack) { if (stack == null) { return false; } else if (this.isEmpty()) { return stack.isEmpty(); } else { for (ItemStack itemStack : this.getItems()) { if (itemStack.is(stack.getItem())) { return true; } } return false; } } public IntList getStackingIds() { if (this.stackingIds == null) { ItemStack[] itemStacks = this.getItems(); this.stackingIds = new IntArrayList(itemStacks.length); for (ItemStack itemStack : itemStacks) { this.stackingIds.add(StackedContents.getStackingIndex(itemStack)); } this.stackingIds.sort(IntComparators.NATURAL_COMPARATOR); } return this.stackingIds; } public boolean isEmpty() { return this.values.length == 0; } public boolean equals(Object object) { return object instanceof Ingredient ingredient ? Arrays.equals(this.values, ingredient.values) : false; } private static Ingredient fromValues(Stream stream) { Ingredient ingredient = new Ingredient(stream); return ingredient.isEmpty() ? EMPTY : ingredient; } public static Ingredient of() { return EMPTY; } public static Ingredient of(ItemLike... items) { return of(Arrays.stream(items).map(ItemStack::new)); } public static Ingredient of(ItemStack... stacks) { return of(Arrays.stream(stacks)); } public static Ingredient of(Stream stacks) { return fromValues(stacks.filter(itemStack -> !itemStack.isEmpty()).map(Ingredient.ItemValue::new)); } public static Ingredient of(TagKey tag) { return fromValues(Stream.of(new Ingredient.TagValue(tag))); } private static Codec codec(boolean allowEmpty) { Codec codec = Codec.list(Ingredient.Value.CODEC) .comapFlatMap( list -> !allowEmpty && list.size() < 1 ? DataResult.error(() -> "Item array cannot be empty, at least one item must be defined") : DataResult.success((Ingredient.Value[])list.toArray(new Ingredient.Value[0])), List::of ); return Codec.either(codec, Ingredient.Value.CODEC) .flatComapMap( either -> either.map(Ingredient::new, value -> new Ingredient(new Ingredient.Value[]{value})), ingredient -> { if (ingredient.values.length == 1) { return DataResult.success(Either.right(ingredient.values[0])); } else { return ingredient.values.length == 0 && !allowEmpty ? DataResult.error(() -> "Item array cannot be empty, at least one item must be defined") : DataResult.success(Either.left(ingredient.values)); } } ); } record ItemValue(ItemStack item) implements Ingredient.Value { static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group(ItemStack.SIMPLE_ITEM_CODEC.fieldOf("item").forGetter(itemValue -> itemValue.item)).apply(instance, Ingredient.ItemValue::new) ); public boolean equals(Object object) { return !(object instanceof Ingredient.ItemValue itemValue) ? false : itemValue.item.getItem().equals(this.item.getItem()) && itemValue.item.getCount() == this.item.getCount(); } @Override public Collection getItems() { return Collections.singleton(this.item); } } record TagValue(TagKey tag) implements Ingredient.Value { static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group(TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter(tagValue -> tagValue.tag)).apply(instance, Ingredient.TagValue::new) ); public boolean equals(Object object) { return object instanceof Ingredient.TagValue tagValue ? tagValue.tag.location().equals(this.tag.location()) : false; } @Override public Collection getItems() { List list = Lists.newArrayList(); for (Holder holder : BuiltInRegistries.ITEM.getTagOrEmpty(this.tag)) { list.add(new ItemStack(holder)); } return list; } } interface Value { Codec CODEC = Codec.xor(Ingredient.ItemValue.CODEC, Ingredient.TagValue.CODEC) .xmap(either -> either.map(itemValue -> itemValue, tagValue -> tagValue), value -> { if (value instanceof Ingredient.TagValue tagValue) { return Either.right(tagValue); } else if (value instanceof Ingredient.ItemValue itemValue) { return Either.left(itemValue); } else { throw new UnsupportedOperationException("This is neither an item value nor a tag value."); } }); Collection getItems(); } }