add fabric-recipe-api-v1: Custom ingredients ()

* Fabric custom ingredient API

* More Ingredient API work

* Optimize shapeless matching logic

* Fix all the things

* Move custom ingredient network serialization to account for ingredient extension API

* Apply suggestions from code review

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>

* Address 🍎 review

* Get rid of the @Overwrite

* Implement graceful fallback for clients not supporting some custom ingredients

* Move custom ingredient code to new Recipe API module

* Fix client package change

* Address review comments

- And/Or -> All/Any
- Move builtin ingredient registration to entrypoint
- Initial protocol version is 1
- Misc other changes

* Add testing instructions

* Use a List for `getMatchingStacks`

* Overengineer ingredient query a bit

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>
Co-authored-by: modmuss50 <modmuss50@gmail.com>
(cherry picked from commit 5176f73dbb)
This commit is contained in:
modmuss50 2023-01-05 13:08:25 +00:00
parent 64bd380d4d
commit 6ed476d6fa
32 changed files with 2055 additions and 1 deletions

View file

@ -6,7 +6,8 @@ moduleDependencies(project, [
'fabric-registry-sync-v0',
'fabric-networking-api-v1',
'fabric-resource-conditions-api-v1',
'fabric-item-groups-v0'
'fabric-item-groups-v0',
'fabric-recipe-api-v1',
])
dependencies {

View file

@ -28,6 +28,9 @@ import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,6 +69,7 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricModelProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.SimpleFabricLootTableProvider;
import net.fabricmc.fabric.api.recipe.v1.ingredient.DefaultCustomIngredients;
import net.fabricmc.fabric.api.resource.conditions.v1.ConditionJsonProvider;
import net.fabricmc.fabric.api.resource.conditions.v1.DefaultResourceConditions;
@ -120,6 +124,73 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
@Override
protected void generateRecipes(Consumer<RecipeJsonProvider> exporter) {
offerPlanksRecipe2(exporter, SIMPLE_BLOCK, ItemTags.ACACIA_LOGS);
/* Generate test recipes using all types of custom ingredients for easy testing */
// Testing procedure for vanilla and fabric clients:
// - Create a new fabric server with the ingredient API.
// - Copy the generated recipes to a datapack, for example to world/datapacks/<packname>/data/test/recipes/.
// - Remember to also include a pack.mcmeta file in world/datapacks/<packname>.
// (see https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack)
// - Start the server and connect to it with a vanilla client.
// - Test all the following recipes
// Test partial NBT
// 1 undamaged pickaxe + 8 pickaxes with any damage value to test shapeless matching logic.
// Interesting test cases:
// - 9 damaged pickaxes should not match.
// - 9 undamaged pickaxes should match.
// - 1 undamaged pickaxe + 8 damaged pickaxes should match (regardless of the position).
// - 1 undamaged renamed pickaxe + 8 damaged pickaxes should match (NBT is not strictly matched here).
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.DIAMOND_BLOCK)
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(DefaultCustomIngredients.nbt(new ItemStack(Items.DIAMOND_PICKAXE), false))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.input(Ingredient.ofItems(Items.DIAMOND_PICKAXE))
.criterion("has_pickaxe", conditionsFromItem(Items.DIAMOND_PICKAXE))
.offerTo(exporter);
// Test strict NBT
// To test: try renaming an apple to "Golden Apple" in creative with an anvil.
// That should match the recipe and give a golden apple. Any other NBT should not match.
ItemStack appleWithGoldenName = new ItemStack(Items.APPLE);
appleWithGoldenName.setCustomName(Text.literal("Golden Apple"));
appleWithGoldenName.setRepairCost(0);
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.GOLDEN_APPLE)
.input(DefaultCustomIngredients.nbt(appleWithGoldenName, true))
.criterion("has_apple", conditionsFromItem(Items.APPLE))
.offerTo(exporter);
// Test AND
// To test: charcoal should give a torch, but coal should not.
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.TORCH)
// charcoal only
.input(DefaultCustomIngredients.all(Ingredient.fromTag(ItemTags.COALS), Ingredient.ofItems(Items.CHARCOAL)))
.criterion("has_charcoal", conditionsFromItem(Items.CHARCOAL))
.offerTo(exporter);
// Test OR
// To test: a golden pickaxe or a golden shovel should give a block of gold.
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.GOLD_BLOCK)
.input(DefaultCustomIngredients.any(Ingredient.ofItems(Items.GOLDEN_PICKAXE), Ingredient.ofItems(Items.GOLDEN_SHOVEL)))
.criterion("has_pickaxe", conditionsFromItem(Items.GOLDEN_PICKAXE))
.criterion("has_shovel", conditionsFromItem(Items.GOLDEN_SHOVEL))
.offerTo(exporter);
// Test difference
// To test: only copper, netherite and emerald should match the recipe.
ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, Items.BEACON)
.input(DefaultCustomIngredients.difference(
DefaultCustomIngredients.any(
Ingredient.fromTag(ItemTags.BEACON_PAYMENT_ITEMS),
Ingredient.ofItems(Items.COPPER_INGOT)),
Ingredient.ofItems(Items.IRON_INGOT, Items.GOLD_INGOT, Items.DIAMOND)))
.criterion("has_payment", conditionsFromTag(ItemTags.BEACON_PAYMENT_ITEMS))
.offerTo(exporter);
}
}

View file

@ -0,0 +1,10 @@
archivesBaseName = "fabric-recipe-api-v1"
version = getSubprojectVersion(project)
loom {
accessWidenerPath = file('src/main/resources/fabric-recipe-api-v1.accesswidener')
}
moduleDependencies(project, [
"fabric-networking-api-v1",
])

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient.client;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientLoginNetworking;
import net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientSync;
/**
* @see CustomIngredientSync
*/
public class CustomIngredientSyncClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ClientLoginNetworking.registerGlobalReceiver(CustomIngredientSync.PACKET_ID, (client, handler, buf, listenerAdder) -> {
int protocolVersion = buf.readVarInt();
return CompletableFuture.completedFuture(CustomIngredientSync.createResponsePacket(protocolVersion));
});
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.recipe.v1.ingredient;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
import net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientImpl;
/**
* Interface that modders can implement to create new behaviors for {@link Ingredient}s.
*
* <p>This is not directly implemented on vanilla {@link Ingredient}s, but conversions are possible:
* <ul>
* <li>{@link #toVanilla()} converts a custom ingredient to a vanilla {@link Ingredient}.</li>
* <li>{@link FabricIngredient} can be used to check if a vanilla {@link Ingredient} is custom,
* and retrieve the custom ingredient in that case.</li>
* </ul>
*
* <p>The format for custom ingredients is as follows:
* <pre>{@code
* {
* "fabric:type": "<identifier of the serializer>",
* // extra ingredient data, dependent on the serializer
* }
* }</pre>
*
* @see CustomIngredientSerializer
*/
public interface CustomIngredient {
/**
* Checks if a stack matches this ingredient.
* The stack <strong>must not</strong> be modified in any way.
*
* @param stack the stack to test
* @return {@code true} if the stack matches this ingredient, {@code false} otherwise
*/
boolean test(ItemStack stack);
/**
* {@return the list of stacks that match this ingredient.}
*
* <p>The following guidelines should be followed for good compatibility:
* <ul>
* <li>These stacks are generally used for display purposes, and need not be exhaustive or perfectly accurate.</li>
* <li>An exception is ingredients that {@linkplain #requiresTesting() don't require testing},
* for which it is important that the returned stacks correspond exactly to all the accepted {@link Item}s.</li>
* <li>At least one stack must be returned for the ingredient not to be considered {@linkplain Ingredient#isEmpty() empty}.</li>
* <li>The ingredient should try to return at least one stack with each accepted {@link Item}.
* This allows mods that inspect the ingredient to figure out which stacks it might accept.</li>
* </ul>
*
* <p>Note: no caching needs to be done by the implementation, this is already handled by the ingredient itself.
*/
List<ItemStack> getMatchingStacks();
/**
* Returns whether this ingredient always requires {@linkplain #test direct stack testing}.
*
* @return {@code false} if this ingredient ignores NBT data when matching stacks, {@code true} otherwise
* @see FabricIngredient#requiresTesting()
*/
boolean requiresTesting();
/**
* {@return the serializer for this ingredient}
*
* <p>The serializer must have been registered using {@link CustomIngredientSerializer#register}.
*/
CustomIngredientSerializer<?> getSerializer();
/**
* {@return a new {@link Ingredient} behaving as defined by this custom ingredient}.
*/
@ApiStatus.NonExtendable
default Ingredient toVanilla() {
return new CustomIngredientImpl(this);
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.recipe.v1.ingredient;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientImpl;
/**
* Serializer for a {@link CustomIngredient}.
*
* <p>All instances must be registered using {@link #register} for deserialization to work.
*
* @param <T> the type of the custom ingredient
*/
public interface CustomIngredientSerializer<T extends CustomIngredient> {
/**
* Registers a custom ingredient serializer, using the {@linkplain CustomIngredientSerializer#getIdentifier() serializer's identifier}.
*
* @throws IllegalArgumentException if the serializer is already registered
*/
static void register(CustomIngredientSerializer<?> serializer) {
CustomIngredientImpl.registerSerializer(serializer);
}
/**
* {@return the custom ingredient serializer registered with the given identifier, or {@code null} if there is no such serializer}.
*/
@Nullable
static CustomIngredientSerializer<?> get(Identifier identifier) {
return CustomIngredientImpl.getSerializer(identifier);
}
/**
* {@return the identifier of this serializer}.
*/
Identifier getIdentifier();
/**
* Deserializes the custom ingredient from a JSON object.
*
* @throws JsonSyntaxException if the JSON object does not match the format expected by the serializer
* @throws IllegalArgumentException if the JSON object is invalid for some other reason
*/
T read(JsonObject json);
/**
* Serializes the custom ingredient to a JSON object.
*/
void write(JsonObject json, T ingredient);
/**
* Deserializes the custom ingredient from a packet buffer.
*/
T read(PacketByteBuf buf);
/**
* Serializes the custom ingredient to a packet buffer.
*/
void write(PacketByteBuf buf, T ingredient);
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.recipe.v1.ingredient;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtHelper;
import net.minecraft.recipe.Ingredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AllIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.DifferenceIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.NbtIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AnyIngredient;
/**
* Factory methods for the custom ingredients directly provided by Fabric API.
*/
public final class DefaultCustomIngredients {
/**
* Creates an ingredient that matches when its sub-ingredients all match.
*
* <p>The JSON format is as follows:
* <pre>{@code
* {
* "fabric:type": "fabric:all",
* "ingredients": [
* // sub-ingredient 1,
* // sub-ingredient 2,
* // etc...
* ]
* }
* }</pre>
*
* @throws IllegalArgumentException if the array is empty
*/
public static Ingredient all(Ingredient... ingredients) {
for (Ingredient ing : ingredients) Objects.requireNonNull(ing, "Ingredient cannot be null");
return new AllIngredient(ingredients).toVanilla();
}
/**
* Creates an ingredient that matches when any of its sub-ingredients matches.
*
* <p>The JSON format is as follows:
* <pre>{@code
* {
* "fabric:type": "fabric:any",
* "ingredients": [
* // sub-ingredient 1,
* // sub-ingredient 2,
* // etc...
* ]
* }
* }</pre>
*
* @throws IllegalArgumentException if the array is empty
*/
public static Ingredient any(Ingredient... ingredients) {
for (Ingredient ing : ingredients) Objects.requireNonNull(ing, "Ingredient cannot be null");
return new AnyIngredient(ingredients).toVanilla();
}
/**
* Creates an ingredient that matches if its base ingredient matches, and its subtracted ingredient <strong>does not</strong> match.
*
* <p>The JSON format is as follows:
* <pre>{@code
* {
* "fabric:type": "fabric:difference",
* "base": // base ingredient,
* "subtracted": // subtracted ingredient
* }
* }</pre>
*/
public static Ingredient difference(Ingredient base, Ingredient subtracted) {
Objects.requireNonNull(base, "Base ingredient cannot be null");
Objects.requireNonNull(subtracted, "Subtracted ingredient cannot be null");
return new DifferenceIngredient(base, subtracted).toVanilla();
}
/**
* Creates an ingredient that wraps another ingredient to also check for stack NBT.
* This check can either be strict (the exact NBT must match) or non-strict aka. partial (the ingredient NBT must be a subset of the stack NBT).
*
* <p>In strict mode, passing a {@code null} {@code nbt} is allowed, and will only match stacks with {@code null} NBT.
* In partial mode, passing a {@code null} {@code nbt} is <strong>not</strong> allowed, as it would always match.
*
* <p>See {@link NbtHelper#matches} for the non-strict matching.
*
* <p>The JSON format is as follows:
* <pre>{@code
* {
* "fabric:type": "fabric:nbt",
* "base": // base ingredient,
* "nbt": // NBT tag to match, either in JSON directly or a string representation (default: null),
* "strict": // whether to use strict matching (default: false)
* }
* }</pre>
*
* @throws IllegalArgumentException if {@code strict} is {@code false} and the NBT is {@code null}
*/
public static Ingredient nbt(Ingredient base, @Nullable NbtCompound nbt, boolean strict) {
Objects.requireNonNull(base, "Base ingredient cannot be null");
return new NbtIngredient(base, nbt, strict).toVanilla();
}
/**
* Creates an ingredient that matches the passed template stack, including NBT.
* Note that the count of the stack is ignored.
*
* @see #nbt(Ingredient, NbtCompound, boolean)
*/
public static Ingredient nbt(ItemStack stack, boolean strict) {
Objects.requireNonNull(stack, "Stack cannot be null");
return nbt(Ingredient.ofItems(stack.getItem()), stack.getNbt(), strict);
}
private DefaultCustomIngredients() {
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.recipe.v1.ingredient;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
/**
* Fabric-provided extensions for {@link Ingredient}.
* This interface is automatically implemented on all ingredients via Mixin and interface injection.
*/
public interface FabricIngredient {
/**
* {@return the backing {@link CustomIngredient} of this ingredient if it's custom, {@code null} otherwise}.
*/
@Nullable
default CustomIngredient getCustomIngredient() {
return null;
}
/**
* Returns whether this ingredient always requires {@linkplain Ingredient#test direct stack testing}.
* Vanilla ingredients will always return {@code false},
* and custom ingredients need to {@linkplain CustomIngredient#requiresTesting() provide this information}.
*
* <p>If {@code false}, {@linkplain Ingredient#test testing this ingredient} with an item stack must be equivalent to checking whether
* the item stack's item is included in the ingredient's {@linkplain Ingredient#getMatchingStacks() list of matching stacks}.
* In that case, optimized matching logic can be used, for example using {@link Ingredient#getMatchingItemIds()}.
*
* <p>If {@code true}, the ingredient must always be tested using {@link Ingredient#test(ItemStack)}.
* Note that Fabric patches some vanilla systems such as shapeless recipes to account for this.
*
* @return {@code false} if this ingredient ignores NBT data when matching stacks, {@code true} otherwise
*/
default boolean requiresTesting() {
return getCustomIngredient() != null && getCustomIngredient().requiresTesting();
}
}

View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.ItemStack;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
/**
* To test this API beyond the unit tests, please refer to the recipe provider in the datagen API testmod.
* It contains various interesting recipes to test, and explains how to package them in a datapack.
*/
public class CustomIngredientImpl extends Ingredient {
// Static helpers used by the API
public static final String TYPE_KEY = "fabric:type";
public static final int PACKET_MARKER = -1;
static final Map<Identifier, CustomIngredientSerializer<?>> REGISTERED_SERIALIZERS = new ConcurrentHashMap<>();
public static void registerSerializer(CustomIngredientSerializer<?> serializer) {
Objects.requireNonNull(serializer.getIdentifier(), "CustomIngredientSerializer identifier may not be null.");
if (REGISTERED_SERIALIZERS.putIfAbsent(serializer.getIdentifier(), serializer) != null) {
throw new IllegalArgumentException("CustomIngredientSerializer with identifier " + serializer.getIdentifier() + " already registered.");
}
}
@Nullable
public static CustomIngredientSerializer<?> getSerializer(Identifier identifier) {
Objects.requireNonNull(identifier, "Identifier may not be null.");
return REGISTERED_SERIALIZERS.get(identifier);
}
// Actual custom ingredient logic
private final CustomIngredient customIngredient;
public CustomIngredientImpl(CustomIngredient customIngredient) {
super(Stream.empty());
this.customIngredient = customIngredient;
}
@Override
public CustomIngredient getCustomIngredient() {
return customIngredient;
}
@Override
public boolean requiresTesting() {
return customIngredient.requiresTesting();
}
@Override
public ItemStack[] getMatchingStacks() {
if (this.matchingStacks == null) {
this.matchingStacks = customIngredient.getMatchingStacks().toArray(ItemStack[]::new);
}
return this.matchingStacks;
}
@Override
public boolean test(@Nullable ItemStack stack) {
return stack != null && customIngredient.test(stack);
}
@Override
public void write(PacketByteBuf buf) {
// Can be null if we're not writing a packet from the PacketEncoder; in that case, always write the full ingredient.
// Chances are this is a mod's doing and the client has the Ingredient API with the relevant ingredients.
Set<Identifier> supportedIngredients = CustomIngredientSync.CURRENT_SUPPORTED_INGREDIENTS.get();
if (supportedIngredients != null && !supportedIngredients.contains(customIngredient.getSerializer().getIdentifier())) {
// The client doesn't support this custom ingredient, so we send the matching stacks as a regular ingredient.
// Conveniently, this is exactly what the super call does.
super.write(buf);
} else {
// The client supports this custom ingredient, so we send it as a custom ingredient.
buf.writeVarInt(PACKET_MARKER);
buf.writeIdentifier(customIngredient.getSerializer().getIdentifier());
customIngredient.getSerializer().write(buf, coerceIngredient());
}
}
@Override
public JsonElement toJson() {
JsonObject json = new JsonObject();
json.addProperty(TYPE_KEY, customIngredient.getSerializer().getIdentifier().toString());
customIngredient.getSerializer().write(json, coerceIngredient());
return json;
}
@Override
public boolean isEmpty() {
// We don't want to resolve the matching stacks,
// as this might cause the ingredient to use outdated tags when it's done too early.
// So we just return false when the matching stacks haven't been resolved yet (i.e. when the field is null).
return matchingStacks != null && matchingStacks.length == 0;
}
private <T> T coerceIngredient() {
return (T) customIngredient;
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AllIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AnyIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.DifferenceIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.NbtIngredient;
/**
* Register builtin custom ingredients.
*/
public class CustomIngredientInit implements ModInitializer {
@Override
public void onInitialize() {
CustomIngredientSerializer.register(AllIngredient.SERIALIZER);
CustomIngredientSerializer.register(AnyIngredient.SERIALIZER);
CustomIngredientSerializer.register(DifferenceIngredient.SERIALIZER);
CustomIngredientSerializer.register(NbtIngredient.SERIALIZER);
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient;
import java.util.HashSet;
import java.util.Set;
import io.netty.channel.ChannelHandler;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.PacketEncoder;
import net.minecraft.util.Identifier;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerLoginNetworking;
import net.fabricmc.fabric.mixin.recipe.ingredient.PacketEncoderMixin;
/**
* To reasonably support server-side only custom ingredients, we only send custom ingredients to clients that support them.
* If a specific client doesn't support a custom ingredient, we send the matching stacks as a regular ingredient.
* This is fine since all recipe computation happens server-side anyway.
*
* <p><ul>
* <li>Each client sends a packet with the set of custom ingredients it supports.</li>
* <li>We store that set inside the {@link PacketEncoder} using {@link PacketEncoderMixin}.</li>
* <li>When serializing a custom ingredient, we get access to the current {@link PacketEncoder},
* and based on that we decide whether to send the custom ingredient, or a vanilla ingredient with the matching stacks.</li>
* </ul>
*/
public class CustomIngredientSync implements ModInitializer {
public static final Identifier PACKET_ID = new Identifier("fabric", "custom_ingredient_sync");
public static final int PROTOCOL_VERSION_1 = 1;
public static final ThreadLocal<Set<Identifier>> CURRENT_SUPPORTED_INGREDIENTS = new ThreadLocal<>();
@Nullable
public static PacketByteBuf createResponsePacket(int serverProtocolVersion) {
if (serverProtocolVersion < PROTOCOL_VERSION_1) {
// Not supposed to happen - notify the server that we didn't understand the query.
return null;
}
// Always send protocol 1 - the server should support it even if it supports more recent protocols.
PacketByteBuf buf = PacketByteBufs.create();
buf.writeVarInt(PROTOCOL_VERSION_1);
buf.writeCollection(CustomIngredientImpl.REGISTERED_SERIALIZERS.keySet(), PacketByteBuf::writeIdentifier);
return buf;
}
public static Set<Identifier> decodeResponsePacket(PacketByteBuf buf) {
int protocolVersion = buf.readVarInt();
switch (protocolVersion) {
case PROTOCOL_VERSION_1 -> {
Set<Identifier> identifiers = buf.readCollection(HashSet::new, PacketByteBuf::readIdentifier);
// Remove unknown keys to save memory
identifiers.removeIf(id -> !CustomIngredientImpl.REGISTERED_SERIALIZERS.containsKey(id));
return identifiers;
}
default -> {
throw new IllegalArgumentException("Unknown ingredient sync protocol version: " + protocolVersion);
}
}
}
@Override
public void onInitialize() {
ServerLoginConnectionEvents.QUERY_START.register((handler, server, sender, synchronizer) -> {
// Send packet with 1 so the client can send us back the list of supported tags.
// 1 is sent in case we need a different protocol later for some reason.
PacketByteBuf buf = PacketByteBufs.create();
buf.writeVarInt(PROTOCOL_VERSION_1); // max supported server protocol version
sender.sendPacket(PACKET_ID, buf);
});
ServerLoginNetworking.registerGlobalReceiver(PACKET_ID, (server, handler, understood, buf, synchronizer, responseSender) -> {
if (!understood) {
// Skip if the client didn't understand the query.
return;
}
Set<Identifier> supportedCustomIngredients = decodeResponsePacket(buf);
ChannelHandler packetEncoder = handler.connection.channel.pipeline().get("encoder");
if (packetEncoder != null) { // Null in singleplayer
((SupportedIngredientsPacketEncoder) packetEncoder).fabric_setSupportedCustomIngredients(supportedCustomIngredients);
}
});
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
/**
* Helper class to perform a shapeless recipe match when ingredients that require testing are involved.
*
* <p>The problem to solve is a maximum cardinality bipartite matching, for which this implementation uses the augmenting path algorithm.
* This has good performance in simple cases, and sufficient O(N^3) asymptotic complexity in the worst case.
*/
public class ShapelessMatch {
private final int[] match;
/**
* The first {@code size} bits are for the visited array (on the left partition).
* The remaining {@code size * size} bits are for the adjacency matrix.
*/
private final BitSet bitSet;
private ShapelessMatch(int size) {
match = new int[size];
bitSet = new BitSet(size * (size+1));
}
private boolean augment(int l) {
if (bitSet.get(l)) return false;
bitSet.set(l);
for (int r = 0; r < match.length; ++r) {
if (bitSet.get(match.length + l * match.length + r)) {
if (match[r] == -1 || augment(match[r])) {
match[r] = l;
return true;
}
}
}
return false;
}
public static boolean isMatch(List<ItemStack> stacks, List<Ingredient> ingredients) {
if (stacks.size() != ingredients.size()) {
return false;
}
ShapelessMatch m = new ShapelessMatch(ingredients.size());
// Build stack -> ingredient bipartite graph
for (int i = 0; i < stacks.size(); ++i) {
ItemStack stack = stacks.get(i);
for (int j = 0; j < ingredients.size(); ++j) {
if (ingredients.get(j).test(stack)) {
m.bitSet.set((i + 1) * m.match.length + j);
}
}
}
// Init matches to -1 (no match)
Arrays.fill(m.match, -1);
// Try to find an augmenting path for each stack
for (int i = 0; i < ingredients.size(); ++i) {
if (!m.augment(i)) {
return false;
}
m.bitSet.set(0, m.match.length, false);
}
return true;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient;
import java.util.Set;
import net.minecraft.network.PacketEncoder;
import net.minecraft.util.Identifier;
/**
* Implemented on {@link PacketEncoder} to store which custom ingredients the client supports.
*/
public interface SupportedIngredientsPacketEncoder {
void fabric_setSupportedCustomIngredients(Set<Identifier> supportedCustomIngredients);
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient.builtin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
public class AllIngredient extends CombinedIngredient {
public static final CustomIngredientSerializer<AllIngredient> SERIALIZER =
new Serializer<>(new Identifier("fabric", "all"), AllIngredient::new);
public AllIngredient(Ingredient[] ingredients) {
super(ingredients);
}
@Override
public boolean test(ItemStack stack) {
for (Ingredient ingredient : ingredients) {
if (!ingredient.test(stack)) {
return false;
}
}
return true;
}
@Override
public List<ItemStack> getMatchingStacks() {
// There's always at least one sub ingredient, so accessing ingredients[0] is safe.
List<ItemStack> previewStacks = new ArrayList<>(Arrays.asList(ingredients[0].getMatchingStacks()));
for (int i = 1; i < ingredients.length; ++i) {
Ingredient ing = ingredients[i];
previewStacks.removeIf(stack -> !ing.test(stack));
}
return previewStacks;
}
@Override
public CustomIngredientSerializer<?> getSerializer() {
return SERIALIZER;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient.builtin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
public class AnyIngredient extends CombinedIngredient {
public static final CustomIngredientSerializer<AnyIngredient> SERIALIZER =
new CombinedIngredient.Serializer<>(new Identifier("fabric", "any"), AnyIngredient::new);
public AnyIngredient(Ingredient[] ingredients) {
super(ingredients);
}
@Override
public boolean test(ItemStack stack) {
for (Ingredient ingredient : ingredients) {
if (ingredient.test(stack)) {
return true;
}
}
return false;
}
@Override
public List<ItemStack> getMatchingStacks() {
List<ItemStack> previewStacks = new ArrayList<>();
for (Ingredient ingredient : ingredients) {
previewStacks.addAll(Arrays.asList(ingredient.getMatchingStacks()));
}
return previewStacks;
}
@Override
public CustomIngredientSerializer<?> getSerializer() {
return SERIALIZER;
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient.builtin;
import java.util.function.Function;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
/**
* Base class for ALL and ANY ingredients.
*/
abstract class CombinedIngredient implements CustomIngredient {
protected final Ingredient[] ingredients;
protected CombinedIngredient(Ingredient[] ingredients) {
if (ingredients.length == 0) {
throw new IllegalArgumentException("ALL or ANY ingredient must have at least one sub-ingredient");
}
this.ingredients = ingredients;
}
@Override
public boolean requiresTesting() {
for (Ingredient ingredient : ingredients) {
if (ingredient.requiresTesting()) {
return true;
}
}
return false;
}
static class Serializer<I extends CombinedIngredient> implements CustomIngredientSerializer<I> {
private final Identifier identifier;
private final Function<Ingredient[], I> factory;
Serializer(Identifier identifier, Function<Ingredient[], I> factory) {
this.identifier = identifier;
this.factory = factory;
}
@Override
public Identifier getIdentifier() {
return identifier;
}
@Override
public I read(JsonObject json) {
JsonArray values = JsonHelper.getArray(json, "ingredients");
Ingredient[] ingredients = new Ingredient[values.size()];
for (int i = 0; i < values.size(); i++) {
ingredients[i] = Ingredient.fromJson(values.get(i));
}
return factory.apply(ingredients);
}
@Override
public void write(JsonObject json, I ingredient) {
JsonArray values = new JsonArray();
for (Ingredient value : ingredient.ingredients) {
values.add(value.toJson());
}
json.add("ingredients", values);
}
@Override
public I read(PacketByteBuf buf) {
int size = buf.readVarInt();
Ingredient[] ingredients = new Ingredient[size];
for (int i = 0; i < size; i++) {
ingredients[i] = Ingredient.fromPacket(buf);
}
return factory.apply(ingredients);
}
@Override
public void write(PacketByteBuf buf, I ingredient) {
buf.writeVarInt(ingredient.ingredients.length);
for (Ingredient value : ingredient.ingredients) {
value.write(buf);
}
}
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient.builtin;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.JsonObject;
import net.minecraft.item.ItemStack;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
public class DifferenceIngredient implements CustomIngredient {
public static final CustomIngredientSerializer<DifferenceIngredient> SERIALIZER = new Serializer();
private final Ingredient base;
private final Ingredient subtracted;
public DifferenceIngredient(Ingredient base, Ingredient subtracted) {
this.base = base;
this.subtracted = subtracted;
}
@Override
public boolean test(ItemStack stack) {
return base.test(stack) && !subtracted.test(stack);
}
@Override
public List<ItemStack> getMatchingStacks() {
List<ItemStack> stacks = new ArrayList<>(List.of(base.getMatchingStacks()));
stacks.removeIf(subtracted);
return stacks;
}
@Override
public boolean requiresTesting() {
return base.requiresTesting() || subtracted.requiresTesting();
}
@Override
public CustomIngredientSerializer<?> getSerializer() {
return SERIALIZER;
}
private static class Serializer implements CustomIngredientSerializer<DifferenceIngredient> {
private final Identifier id = new Identifier("fabric", "difference");
@Override
public Identifier getIdentifier() {
return id;
}
@Override
public DifferenceIngredient read(JsonObject json) {
Ingredient base = Ingredient.fromJson(json.get("base"));
Ingredient subtracted = Ingredient.fromJson(json.get("subtracted"));
return new DifferenceIngredient(base, subtracted);
}
@Override
public void write(JsonObject json, DifferenceIngredient ingredient) {
json.add("base", ingredient.base.toJson());
json.add("subtracted", ingredient.subtracted.toJson());
}
@Override
public DifferenceIngredient read(PacketByteBuf buf) {
Ingredient base = Ingredient.fromPacket(buf);
Ingredient subtracted = Ingredient.fromPacket(buf);
return new DifferenceIngredient(base, subtracted);
}
@Override
public void write(PacketByteBuf buf, DifferenceIngredient ingredient) {
ingredient.base.write(buf);
ingredient.subtracted.write(buf);
}
}
}

View file

@ -0,0 +1,167 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.recipe.ingredient.builtin;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.serialization.JsonOps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtHelper;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.StringNbtReader;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.predicate.NbtPredicate;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
public class NbtIngredient implements CustomIngredient {
public static final CustomIngredientSerializer<NbtIngredient> SERIALIZER = new Serializer();
private final Ingredient base;
@Nullable
private final NbtCompound nbt;
private final boolean strict;
public NbtIngredient(Ingredient base, @Nullable NbtCompound nbt, boolean strict) {
if (nbt == null && !strict) {
throw new IllegalArgumentException("NbtIngredient can only have null NBT in strict mode");
}
this.base = base;
this.nbt = nbt;
this.strict = strict;
}
@Override
public boolean test(ItemStack stack) {
if (!base.test(stack)) return false;
if (strict) {
return Objects.equals(nbt, stack.getNbt());
} else {
return NbtHelper.matches(nbt, stack.getNbt(), true);
}
}
@Override
public List<ItemStack> getMatchingStacks() {
List<ItemStack> stacks = new ArrayList<>(List.of(base.getMatchingStacks()));
stacks.replaceAll(stack -> {
ItemStack copy = stack.copy();
if (nbt != null) {
copy.setNbt(nbt.copy());
}
return copy;
});
stacks.removeIf(stack -> !base.test(stack));
return stacks;
}
@Override
public boolean requiresTesting() {
return true;
}
@Override
public CustomIngredientSerializer<?> getSerializer() {
return SERIALIZER;
}
private static class Serializer implements CustomIngredientSerializer<NbtIngredient> {
private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
private final Identifier id = new Identifier("fabric", "nbt");
@Override
public Identifier getIdentifier() {
return id;
}
@Override
public NbtIngredient read(JsonObject json) {
Ingredient base = Ingredient.fromJson(json.get("base"));
NbtCompound nbt = readNbt(json.get("nbt"));
boolean strict = JsonHelper.getBoolean(json, "strict", false);
return new NbtIngredient(base, nbt, strict);
}
/**
* Inspiration taken from {@link NbtPredicate#fromJson}.
*/
@Nullable
private static NbtCompound readNbt(@Nullable JsonElement json) {
// Process null
if (json == null || json.isJsonNull()) {
return null;
}
try {
if (json.isJsonObject()) {
// We use a normal .toString() to convert the json to string, and read it as SNBT.
// Using DynamicOps would mess with the type of integers and cause things like damage comparisons to fail...
return StringNbtReader.parse(json.toString());
} else {
// Assume it's a string representation of the NBT
return StringNbtReader.parse(JsonHelper.asString(json, "nbt"));
}
} catch (CommandSyntaxException commandSyntaxException) {
throw new JsonSyntaxException("Invalid nbt tag: " + commandSyntaxException.getMessage());
}
}
@Override
public void write(JsonObject json, NbtIngredient ingredient) {
json.add("base", ingredient.base.toJson());
json.addProperty("strict", ingredient.strict);
if (ingredient.nbt != null) {
json.add("nbt", NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, ingredient.nbt));
}
}
@Override
public NbtIngredient read(PacketByteBuf buf) {
Ingredient base = Ingredient.fromPacket(buf);
NbtCompound nbt = buf.readNbt();
boolean strict = buf.readBoolean();
return new NbtIngredient(base, nbt, strict);
}
@Override
public void write(PacketByteBuf buf, NbtIngredient ingredient) {
ingredient.base.write(buf);
buf.writeNbt(ingredient.nbt);
buf.writeBoolean(ingredient.strict);
}
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.recipe.ingredient;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
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;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.recipe.Ingredient;
import net.minecraft.util.Identifier;
import net.minecraft.util.JsonHelper;
import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer;
import net.fabricmc.fabric.api.recipe.v1.ingredient.FabricIngredient;
import net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientImpl;
import net.fabricmc.fabric.impl.recipe.ingredient.builtin.AnyIngredient;
@Mixin(Ingredient.class)
public class IngredientMixin implements FabricIngredient {
/**
* Inject right when vanilla detected a json object and check for our custom key.
*/
@Inject(
at = @At(
value = "INVOKE",
target = "net/minecraft/recipe/Ingredient.entryFromJson (Lcom/google/gson/JsonObject;)Lnet/minecraft/recipe/Ingredient$Entry;",
ordinal = 0
),
method = "fromJson",
cancellable = true
)
private static void injectFromJson(JsonElement json, CallbackInfoReturnable<Ingredient> cir) {
JsonObject obj = json.getAsJsonObject();
if (obj.has(CustomIngredientImpl.TYPE_KEY)) {
Identifier id = new Identifier(JsonHelper.getString(obj, CustomIngredientImpl.TYPE_KEY));
CustomIngredientSerializer<?> serializer = CustomIngredientSerializer.get(id);
if (serializer != null) {
cir.setReturnValue(serializer.read(obj).toVanilla());
} else {
throw new IllegalArgumentException("Unknown custom ingredient type: " + id);
}
}
}
/**
* Throw exception when someone attempts to use our custom key inside an array ingredient.
* The {@link AnyIngredient} should be used instead.
*/
@Inject(at = @At("HEAD"), method = "entryFromJson")
private static void injectEntryFromJson(JsonObject obj, CallbackInfoReturnable<?> cir) {
if (obj.has(CustomIngredientImpl.TYPE_KEY)) {
throw new IllegalArgumentException("Custom ingredient cannot be used inside an array ingredient. You can replace the array by a fabric:any ingredient.");
}
}
@Inject(
at = @At("HEAD"),
method = "fromPacket",
cancellable = true
)
private static void injectFromPacket(PacketByteBuf buf, CallbackInfoReturnable<Ingredient> cir) {
int index = buf.readerIndex();
if (buf.readVarInt() == CustomIngredientImpl.PACKET_MARKER) {
Identifier type = buf.readIdentifier();
CustomIngredientSerializer<?> serializer = CustomIngredientSerializer.get(type);
if (serializer == null) {
throw new IllegalArgumentException("Cannot deserialize custom ingredient of unknown type " + type);
}
cir.setReturnValue(serializer.read(buf).toVanilla());
} else {
// Reset index for vanilla's normal deserialization logic.
buf.readerIndex(index);
}
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.recipe.ingredient;
import java.util.Set;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import org.spongepowered.asm.mixin.Mixin;
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.CallbackInfo;
import net.minecraft.network.Packet;
import net.minecraft.network.PacketEncoder;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientSync;
import net.fabricmc.fabric.impl.recipe.ingredient.SupportedIngredientsPacketEncoder;
@Mixin(PacketEncoder.class)
public class PacketEncoderMixin implements SupportedIngredientsPacketEncoder {
@Unique
private Set<Identifier> fabric_supportedCustomIngredients = Set.of();
@Override
public void fabric_setSupportedCustomIngredients(Set<Identifier> supportedCustomIngredients) {
fabric_supportedCustomIngredients = supportedCustomIngredients;
}
@Inject(
at = @At(
value = "INVOKE",
target = "net/minecraft/network/Packet.write(Lnet/minecraft/network/PacketByteBuf;)V"
),
method = "encode(Lio/netty/channel/ChannelHandlerContext;Lnet/minecraft/network/Packet;Lio/netty/buffer/ByteBuf;)V"
)
private void capturePacketEncoder(ChannelHandlerContext channelHandlerContext, Packet<?> packet, ByteBuf byteBuf, CallbackInfo ci) {
CustomIngredientSync.CURRENT_SUPPORTED_INGREDIENTS.set(fabric_supportedCustomIngredients);
}
@Inject(
at = {
// Normal target after writing
@At(
value = "INVOKE",
target = "net/minecraft/network/Packet.write(Lnet/minecraft/network/PacketByteBuf;)V",
shift = At.Shift.AFTER,
by = 1
),
// In the catch handler in case some exception was thrown
@At(
value = "INVOKE",
target = "net/minecraft/network/Packet.isWritingErrorSkippable()Z"
)
},
method = "encode(Lio/netty/channel/ChannelHandlerContext;Lnet/minecraft/network/Packet;Lio/netty/buffer/ByteBuf;)V"
)
private void releasePacketEncoder(ChannelHandlerContext channelHandlerContext, Packet<?> packet, ByteBuf byteBuf, CallbackInfo ci) {
CustomIngredientSync.CURRENT_SUPPORTED_INGREDIENTS.set(null);
}
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.recipe.ingredient;
import java.util.ArrayList;
import java.util.List;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
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.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.inventory.CraftingInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.Ingredient;
import net.minecraft.recipe.ShapelessRecipe;
import net.minecraft.recipe.book.CraftingRecipeCategory;
import net.minecraft.util.Identifier;
import net.minecraft.util.collection.DefaultedList;
import net.minecraft.world.World;
import net.fabricmc.fabric.impl.recipe.ingredient.ShapelessMatch;
@Mixin(ShapelessRecipe.class)
public class ShapelessRecipeMixin {
@Final
@Shadow
DefaultedList<Ingredient> input;
@Unique
private boolean fabric_requiresTesting = false;
@Inject(at = @At("RETURN"), method = "<init>")
private void cacheRequiresTesting(Identifier id, String group, CraftingRecipeCategory category, ItemStack output, DefaultedList<Ingredient> input, CallbackInfo ci) {
for (Ingredient ingredient : input) {
if (ingredient.requiresTesting()) {
fabric_requiresTesting = true;
break;
}
}
}
@Inject(at = @At("HEAD"), method = "matches", cancellable = true)
public void customIngredientMatch(CraftingInventory craftingInventory, World world, CallbackInfoReturnable<Boolean> cir) {
if (fabric_requiresTesting) {
List<ItemStack> nonEmptyStacks = new ArrayList<>(craftingInventory.size());
for (int i = 0; i < craftingInventory.size(); ++i) {
ItemStack stack = craftingInventory.getStack(i);
if (!stack.isEmpty()) {
nonEmptyStacks.add(stack);
}
}
cir.setReturnValue(ShapelessMatch.isMatch(nonEmptyStacks, input));
}
}
}

Binary file not shown.

After

(image error) Size: 1.5 KiB

View file

@ -0,0 +1,7 @@
accessWidener v2 named
extendable class net/minecraft/recipe/Ingredient
accessible method net/minecraft/recipe/Ingredient <init> (Ljava/util/stream/Stream;)V
accessible field net/minecraft/recipe/Ingredient matchingStacks [Lnet/minecraft/item/ItemStack;
accessible field net/minecraft/network/ClientConnection channel Lio/netty/channel/Channel;

View file

@ -0,0 +1,13 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.recipe",
"compatibilityLevel": "JAVA_17",
"mixins": [
"ingredient.IngredientMixin",
"ingredient.PacketEncoderMixin",
"ingredient.ShapelessRecipeMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View file

@ -0,0 +1,42 @@
{
"schemaVersion": 1,
"id": "fabric-recipe-api-v1",
"name": "Fabric Recipe API (v1)",
"version": "${version}",
"environment": "*",
"license": "Apache-2.0",
"icon": "assets/fabric-recipe-api-v1/icon.png",
"contact": {
"homepage": "https://fabricmc.net",
"irc": "irc://irc.esper.net:6667/fabric",
"issues": "https://github.com/FabricMC/fabric/issues",
"sources": "https://github.com/FabricMC/fabric"
},
"authors": [
"FabricMC"
],
"mixins": [
"fabric-recipe-api-v1.mixins.json"
],
"accessWidener": "fabric-recipe-api-v1.accesswidener",
"depends": {
"fabricloader": ">=0.14.10",
"fabric-networking-api-v1": "*"
},
"entrypoints": {
"main": [
"net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientInit",
"net.fabricmc.fabric.impl.recipe.ingredient.CustomIngredientSync"
],
"client": [
"net.fabricmc.fabric.impl.recipe.ingredient.client.CustomIngredientSyncClient"
]
},
"description": "Recipe extensions such as creation of new types of recipe ingredients.",
"custom": {
"fabric-api:module-lifecycle": "stable",
"loom:injected_interfaces": {
"net/minecraft/class_1856": ["net/fabricmc/fabric/api/recipe/v1/ingredient/FabricIngredient"]
}
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.recipe.ingredient;
import java.util.List;
import java.util.Objects;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.recipe.Ingredient;
import net.minecraft.test.GameTest;
import net.minecraft.test.GameTestException;
import net.minecraft.test.TestContext;
import net.minecraft.text.Text;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
import net.fabricmc.fabric.api.recipe.v1.ingredient.DefaultCustomIngredients;
public class IngredientMatchTests {
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testAllIngredient(TestContext context) {
Ingredient allIngredient = DefaultCustomIngredients.all(Ingredient.ofItems(Items.APPLE, Items.CARROT), Ingredient.ofItems(Items.STICK, Items.CARROT));
assertEquals(1, allIngredient.getMatchingStacks().length);
assertEquals(Items.CARROT, allIngredient.getMatchingStacks()[0].getItem());
assertEquals(false, allIngredient.isEmpty());
assertEquals(false, allIngredient.test(new ItemStack(Items.APPLE)));
assertEquals(true, allIngredient.test(new ItemStack(Items.CARROT)));
assertEquals(false, allIngredient.test(new ItemStack(Items.STICK)));
Ingredient emptyAllIngredient = DefaultCustomIngredients.all(Ingredient.ofItems(Items.APPLE), Ingredient.ofItems(Items.STICK));
assertEquals(0, emptyAllIngredient.getMatchingStacks().length);
assertEquals(true, emptyAllIngredient.isEmpty());
assertEquals(false, emptyAllIngredient.test(new ItemStack(Items.APPLE)));
assertEquals(false, emptyAllIngredient.test(new ItemStack(Items.STICK)));
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testAnyIngredient(TestContext context) {
Ingredient anyIngredient = DefaultCustomIngredients.any(Ingredient.ofItems(Items.APPLE, Items.CARROT), Ingredient.ofItems(Items.STICK, Items.CARROT));
assertEquals(4, anyIngredient.getMatchingStacks().length);
assertEquals(Items.APPLE, anyIngredient.getMatchingStacks()[0].getItem());
assertEquals(Items.CARROT, anyIngredient.getMatchingStacks()[1].getItem());
assertEquals(Items.STICK, anyIngredient.getMatchingStacks()[2].getItem());;
assertEquals(Items.CARROT, anyIngredient.getMatchingStacks()[3].getItem());
assertEquals(false, anyIngredient.isEmpty());
assertEquals(true, anyIngredient.test(new ItemStack(Items.APPLE)));
assertEquals(true, anyIngredient.test(new ItemStack(Items.CARROT)));
assertEquals(true, anyIngredient.test(new ItemStack(Items.STICK)));
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testDifferenceIngredient(TestContext context) {
Ingredient differenceIngredient = DefaultCustomIngredients.difference(Ingredient.ofItems(Items.APPLE, Items.CARROT), Ingredient.ofItems(Items.STICK, Items.CARROT));
assertEquals(1, differenceIngredient.getMatchingStacks().length);
assertEquals(Items.APPLE, differenceIngredient.getMatchingStacks()[0].getItem());
assertEquals(false, differenceIngredient.isEmpty());
assertEquals(true, differenceIngredient.test(new ItemStack(Items.APPLE)));
assertEquals(false, differenceIngredient.test(new ItemStack(Items.CARROT)));
assertEquals(false, differenceIngredient.test(new ItemStack(Items.STICK)));
context.complete();
}
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testNbtIngredient(TestContext context) {
for (boolean strict : List.of(true, false)) {
NbtCompound undamagedNbt = new NbtCompound();
undamagedNbt.putInt(ItemStack.DAMAGE_KEY, 0);
Ingredient nbtIngredient = DefaultCustomIngredients.nbt(Ingredient.ofItems(Items.DIAMOND_PICKAXE, Items.NETHERITE_PICKAXE), undamagedNbt, strict);
assertEquals(2, nbtIngredient.getMatchingStacks().length);
assertEquals(Items.DIAMOND_PICKAXE, nbtIngredient.getMatchingStacks()[0].getItem());
assertEquals(Items.NETHERITE_PICKAXE, nbtIngredient.getMatchingStacks()[1].getItem());
assertEquals(undamagedNbt, nbtIngredient.getMatchingStacks()[0].getNbt());
assertEquals(undamagedNbt, nbtIngredient.getMatchingStacks()[1].getNbt());
assertEquals(false, nbtIngredient.isEmpty());
// Undamaged is fine
assertEquals(true, nbtIngredient.test(new ItemStack(Items.DIAMOND_PICKAXE)));
assertEquals(true, nbtIngredient.test(new ItemStack(Items.NETHERITE_PICKAXE)));
// Damaged is not fine
ItemStack damagedDiamondPickaxe = new ItemStack(Items.DIAMOND_PICKAXE);
damagedDiamondPickaxe.setDamage(10);
assertEquals(false, nbtIngredient.test(damagedDiamondPickaxe));
// Renamed undamaged is only fine in partial matching
ItemStack renamedUndamagedDiamondPickaxe = new ItemStack(Items.DIAMOND_PICKAXE);
renamedUndamagedDiamondPickaxe.setCustomName(Text.literal("Renamed"));
assertEquals(!strict, nbtIngredient.test(renamedUndamagedDiamondPickaxe));
}
// Also test strict null NBT matching
Ingredient noNbtIngredient = DefaultCustomIngredients.nbt(Ingredient.ofItems(Items.APPLE), null, true);
assertEquals(1, noNbtIngredient.getMatchingStacks().length);
assertEquals(Items.APPLE, noNbtIngredient.getMatchingStacks()[0].getItem());
assertEquals(null, noNbtIngredient.getMatchingStacks()[0].getNbt());
assertEquals(false, noNbtIngredient.isEmpty());
// No NBT is fine
assertEquals(true, noNbtIngredient.test(new ItemStack(Items.APPLE)));
// NBT is not fine
ItemStack nbtApple = new ItemStack(Items.APPLE);
nbtApple.setCustomName(Text.literal("Renamed"));
assertEquals(false, noNbtIngredient.test(nbtApple));
context.complete();
}
private static <T> void assertEquals(T expected, T actual) {
if (!Objects.equals(expected, actual)) {
throw new GameTestException(String.format("assertEquals failed%nexpected: %s%n but was: %s", expected, actual));
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.recipe.ingredient;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import net.minecraft.recipe.Ingredient;
import net.minecraft.test.GameTest;
import net.minecraft.test.GameTestException;
import net.minecraft.test.TestContext;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
public class SerializationTests {
/**
* Check that trying to use a custom ingredient inside an array ingredient fails.
*/
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testArrayDeserialization(TestContext context) {
String ingredientJson = """
[
{
"fabric:type": "fabric:all",
"ingredients": [
{
"item": "minecraft:stone"
}
]
}, {
"item": "minecraft:dirt"
}
]
""";
JsonElement json = JsonParser.parseString(ingredientJson);
try {
Ingredient.fromJson(json);
throw new GameTestException("Using a custom ingredient inside an array ingredient should have failed.");
} catch (IllegalArgumentException e) {
context.complete();
}
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.recipe.ingredient;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.inventory.CraftingInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.recipe.ShapelessRecipe;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.test.GameTest;
import net.minecraft.test.GameTestException;
import net.minecraft.test.TestContext;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
public class ShapelessRecipeMatchTests {
/**
* The recipe requires at least one undamaged pickaxe.
*/
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testShapelessMatch(TestContext context) {
Identifier recipeId = new Identifier("fabric-recipe-api-v1-testmod", "test_shapeless_match");
ShapelessRecipe recipe = (ShapelessRecipe) context.getWorld().getRecipeManager().get(recipeId).get();
ItemStack undamagedPickaxe = new ItemStack(Items.DIAMOND_PICKAXE);
ItemStack damagedPickaxe = new ItemStack(Items.DIAMOND_PICKAXE);
damagedPickaxe.setDamage(100);
CraftingInventory craftingInv = new CraftingInventory(new ScreenHandler(null, 0) {
@Override
public ItemStack quickMove(PlayerEntity player, int slot) {
return ItemStack.EMPTY;
}
@Override
public boolean canUse(PlayerEntity player) {
return false;
}
}, 3, 3);
// Test that damaged only doesn't work
for (int i = 0; i < 9; ++i) {
craftingInv.setStack(i, damagedPickaxe);
}
if (recipe.matches(craftingInv, context.getWorld())) {
throw new GameTestException("Recipe should not match with only damaged pickaxes");
}
craftingInv.setStack(1, undamagedPickaxe);
if (!recipe.matches(craftingInv, context.getWorld())) {
throw new GameTestException("Recipe should match with at least one undamaged pickaxe");
}
context.complete();
}
}

View file

@ -0,0 +1,42 @@
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
{
"item": "minecraft:diamond_pickaxe"
},
{
"item": "minecraft:diamond_pickaxe"
},
{
"item": "minecraft:diamond_pickaxe"
},
{
"item": "minecraft:diamond_pickaxe"
},
{
"fabric:type": "fabric:nbt",
"base": {
"item": "minecraft:diamond_pickaxe"
},
"nbt": {
"Damage": 0
},
"strict": false
},
{
"item": "minecraft:diamond_pickaxe"
},
{
"item": "minecraft:diamond_pickaxe"
},
{
"item": "minecraft:diamond_pickaxe"
},
{
"item": "minecraft:diamond_pickaxe"
}
],
"result": {
"item": "minecraft:diamond_block"
}
}

View file

@ -0,0 +1,18 @@
{
"schemaVersion": 1,
"id": "fabric-recipe-api-v1-testmod",
"name": "Fabric Recipe API (v1) Test Mod",
"version": "1.0.0",
"environment": "*",
"license": "Apache-2.0",
"depends": {
"fabric-recipe-api-v1":"*"
},
"entrypoints": {
"fabric-gametest": [
"net.fabricmc.fabric.test.recipe.ingredient.IngredientMatchTests",
"net.fabricmc.fabric.test.recipe.ingredient.SerializationTests",
"net.fabricmc.fabric.test.recipe.ingredient.ShapelessRecipeMatchTests"
]
}
}

View file

@ -41,6 +41,7 @@ fabric-networking-api-v1-version=1.2.10
fabric-networking-v0-version=0.3.27
fabric-object-builder-api-v1-version=4.1.5
fabric-particles-v1-version=1.0.13
fabric-recipe-api-v1-version=1.0.0
fabric-registry-sync-v0-version=0.9.31
fabric-renderer-api-v1-version=1.2.0
fabric-renderer-indigo-version=0.7.0

View file

@ -37,6 +37,7 @@ include 'fabric-models-v0'
include 'fabric-networking-api-v1'
include 'fabric-object-builder-api-v1'
include 'fabric-particles-v1'
include 'fabric-recipe-api-v1'
include 'fabric-registry-sync-v0'
include 'fabric-renderer-api-v1'
include 'fabric-renderer-indigo'