From d8cf4e5a102ee23ddd838975ecddf5cf1bbffa2c Mon Sep 17 00:00:00 2001 From: AlphaMode <26313415+alphamode@users.noreply.github.com> Date: Sun, 20 Nov 2022 07:25:23 -0600 Subject: [PATCH] Support stack aware recipe remainders (#2556) * Support stack aware recipe remainders * Fix checkstyle * Remove all overwrites * Add FabricItemStack and make RecipeRemainderHandler thread safe * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com> * Remove hasRecipeRemainder, Update test mod and remove unneeded mixins * Update fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java Co-authored-by: Salvatore Peluso <info@devpelux.xyz> * Avoid copying the ItemStack * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java Co-authored-by: Salvatore Peluso <info@devpelux.xyz> * Sneakily change duplicate keybinding to a less used key * make everything thread safe and improve AbstractFurnaceBlockEntityMixin * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com> * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: Salvatore Peluso <info@devpelux.xyz> * clear thread local and change field prefix * forgot the allow * Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java Co-authored-by: Salvatore Peluso <info@devpelux.xyz> * Update fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com> * Add FurnaceGameTest * Change test keybind back to LShift * Fix brewing stand remainder and fix nitpicks * add code example to remainder javadoc * Fixed and reformatted docs, changed recipe mixin behavior to store the remainder stack instead of the original stack, refactoring. * Added gametests for brewing stand and recipe mixins, fixed furnace gametest compairing stacks with themselves. * Use (0,1,0) position for game tests * Review changes Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com> Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com> Co-authored-by: Salvatore Peluso <info@devpelux.xyz> Co-authored-by: modmuss50 <modmuss50@gmail.com> (cherry picked from commit fa140d597616a8dee53622bce701a5c0ac8810b9) --- fabric-item-api-v1/build.gradle | 4 + .../fabric/api/item/v1/FabricItem.java | 32 ++++ .../fabric/api/item/v1/FabricItemStack.java | 39 +++++ .../impl/item/RecipeRemainderHandler.java | 26 +++ .../item/AbstractFurnaceBlockEntityMixin.java | 50 ++++++ .../item/BrewingStandBlockEntityMixin.java | 59 +++++++ .../fabric/mixin/item/ItemStackMixin.java | 3 +- .../fabric/mixin/item/RecipeMixin.java | 52 ++++++ .../resources/fabric-item-api-v1.mixins.json | 7 +- .../src/main/resources/fabric.mod.json | 3 +- .../fabric/test/item/CustomDamageTest.java | 22 ++- .../item/gametest/BrewingStandGameTest.java | 149 ++++++++++++++++++ .../test/item/gametest/FurnaceGameTest.java | 122 ++++++++++++++ .../test/item/gametest/RecipeGameTest.java | 144 +++++++++++++++++ .../mixin/BrewingRecipeRegistryAccessor.java | 32 ++++ .../recipes/weird_pickaxe.json | 15 ++ .../fabric-item-api-tests-v1.mixins.json | 11 ++ .../src/testmod/resources/fabric.mod.json | 10 +- 18 files changed, 774 insertions(+), 6 deletions(-) create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java create mode 100644 fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json create mode 100644 fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json diff --git a/fabric-item-api-v1/build.gradle b/fabric-item-api-v1/build.gradle index 692eb5ead..1c1ca60c1 100644 --- a/fabric-item-api-v1/build.gradle +++ b/fabric-item-api-v1/build.gradle @@ -4,3 +4,7 @@ version = getSubprojectVersion(project) moduleDependencies(project, [ 'fabric-api-base' ]) + +dependencies { + testmodImplementation project(path: ':fabric-content-registries-v0', configuration: 'namedElements') +} diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java index 547cb644c..847dc8dc4 100644 --- a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java @@ -91,4 +91,36 @@ public interface FabricItem { default boolean isSuitableFor(ItemStack stack, BlockState state) { return ((Item) this).isSuitableFor(state); } + + /** + * Returns a leftover item stack after {@code stack} is consumed in a recipe. + * (This is also known as "recipe remainder".) + * For example, using a lava bucket in a furnace as fuel will leave an empty bucket. + * + * <p>Here is an example for a recipe remainder that increments the item's damage. + * + * <pre> + * if (stack.getDamage() < stack.getMaxDamage() - 1) { + * ItemStack moreDamaged = stack.copy(); + * moreDamaged.setDamage(stack.getDamage() + 1); + * return moreDamaged; + * } + * + * return ItemStack.EMPTY; + * </pre> + * + * + * <p>This is a stack-aware version of {@link Item#getRecipeRemainder()}. + * + * <p>Note that simple item remainders can also be set via {@link Item.Settings#recipeRemainder(Item)}. + * + * <p>If you want to get a remainder for a stack, + * is recommended to use the stack version of this method: {@link FabricItemStack#getRecipeRemainder()}. + * + * @param stack the consumed {@link ItemStack} + * @return the leftover item stack + */ + default ItemStack getRecipeRemainder(ItemStack stack) { + return ((Item) this).hasRecipeRemainder() ? ((Item) this).getRecipeRemainder().getDefaultStack() : ItemStack.EMPTY; + } } diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java new file mode 100644 index 000000000..0c646a6aa --- /dev/null +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java @@ -0,0 +1,39 @@ +/* + * 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.item.v1; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +/* + * Fabric-provided extensions for {@link ItemStack}. + * This interface is automatically implemented on all item stacks via Mixin and interface injection. + */ +public interface FabricItemStack { + /** + * Return a leftover item for use in recipes. + * + * <p>See {@link FabricItem#getRecipeRemainder(ItemStack)} for a more in depth description. + * + * <p>Stack-aware version of {@link Item#getRecipeRemainder()}. + * + * @return the leftover item + */ + default ItemStack getRecipeRemainder() { + return ((ItemStack) this).getItem().getRecipeRemainder((ItemStack) this); + } +} diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java new file mode 100644 index 000000000..07c262795 --- /dev/null +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java @@ -0,0 +1,26 @@ +/* + * 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.item; + +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.item.ItemStack; + +@ApiStatus.Internal +public class RecipeRemainderHandler { + public static final ThreadLocal<ItemStack> REMAINDER_STACK = new ThreadLocal<>(); +} diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java new file mode 100644 index 000000000..b881e0643 --- /dev/null +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java @@ -0,0 +1,50 @@ +/* + * 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.item; + +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.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Recipe; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +@Mixin(AbstractFurnaceBlockEntity.class) +public abstract class AbstractFurnaceBlockEntityMixin { + @Unique + private static final ThreadLocal<ItemStack> REMAINDER_STACK = new ThreadLocal<>(); + + @Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItem()Lnet/minecraft/item/Item;"), locals = LocalCapture.CAPTURE_FAILHARD, allow = 1) + private static void getStackRemainder(World world, BlockPos pos, BlockState state, AbstractFurnaceBlockEntity blockEntity, CallbackInfo ci, boolean bl, boolean bl2, ItemStack itemStack, Recipe recipe, int i) { + REMAINDER_STACK.set(itemStack.getRecipeRemainder()); + } + + @ModifyArg(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/DefaultedList;set(ILjava/lang/Object;)Ljava/lang/Object;"), index = 1, allow = 1) + private static <E> E setStackRemainder(E element) { + E remainder = (E) REMAINDER_STACK.get(); + REMAINDER_STACK.remove(); + return remainder; + } +} diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java new file mode 100644 index 000000000..fdc37fcb1 --- /dev/null +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java @@ -0,0 +1,59 @@ +/* + * 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.item; + +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.ModifyVariable; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import net.minecraft.block.entity.BrewingStandBlockEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +@Mixin(BrewingStandBlockEntity.class) +public class BrewingStandBlockEntityMixin { + @Unique + private static final ThreadLocal<ItemStack> REMAINDER_STACK = new ThreadLocal<>(); + + @Inject(method = "craft", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;decrement(I)V"), locals = LocalCapture.CAPTURE_FAILHARD) + private static void captureItemStack(World world, BlockPos pos, DefaultedList<ItemStack> slots, CallbackInfo ci, ItemStack itemStack) { + REMAINDER_STACK.set(itemStack.getRecipeRemainder()); + } + + @Redirect(method = "craft", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/Item;hasRecipeRemainder()Z")) + private static boolean hasStackRecipeRemainder(Item instance) { + return !REMAINDER_STACK.get().isEmpty(); + } + + /** + * Injected after the {@link Item#getRecipeRemainder} to replace the old remainder with are new one. + */ + @ModifyVariable(method = "craft", at = @At(value = "STORE"), index = 4) + private static ItemStack createStackRecipeRemainder(ItemStack old) { + ItemStack remainder = REMAINDER_STACK.get(); + REMAINDER_STACK.remove(); + return remainder; + } +} diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java index eb88e7380..08424e4ab 100644 --- a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java @@ -37,12 +37,13 @@ import net.minecraft.entity.attribute.EntityAttributeModifier; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.fabricmc.fabric.api.item.v1.FabricItemStack; import net.fabricmc.fabric.api.item.v1.CustomDamageHandler; import net.fabricmc.fabric.api.item.v1.ModifyItemAttributeModifiersCallback; import net.fabricmc.fabric.impl.item.ItemExtensions; @Mixin(ItemStack.class) -public abstract class ItemStackMixin { +public abstract class ItemStackMixin implements FabricItemStack { @Shadow public abstract Item getItem(); @Unique diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java new file mode 100644 index 000000000..95a73b109 --- /dev/null +++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java @@ -0,0 +1,52 @@ +/* + * 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.item; + +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import net.minecraft.inventory.Inventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.recipe.Recipe; +import net.minecraft.util.collection.DefaultedList; + +import net.fabricmc.fabric.impl.item.RecipeRemainderHandler; + +@Mixin(Recipe.class) +public interface RecipeMixin<C extends Inventory> { + @Inject(method = "getRemainder", at = @At(value = "INVOKE", target = "Lnet/minecraft/inventory/Inventory;getStack(I)Lnet/minecraft/item/ItemStack;"), locals = LocalCapture.CAPTURE_FAILHARD) + default void captureStack(C inventory, CallbackInfoReturnable<DefaultedList<ItemStack>> cir, DefaultedList<ItemStack> defaultedList, int i) { + RecipeRemainderHandler.REMAINDER_STACK.set(inventory.getStack(i).getRecipeRemainder()); + } + + @Redirect(method = "getRemainder", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/Item;hasRecipeRemainder()Z")) + private boolean hasStackRemainder(Item instance) { + return !RecipeRemainderHandler.REMAINDER_STACK.get().isEmpty(); + } + + @Redirect(method = "getRemainder", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/DefaultedList;set(ILjava/lang/Object;)Ljava/lang/Object;")) + private Object getStackRemainder(DefaultedList<ItemStack> inventory, int index, Object element) { + Object remainder = inventory.set(index, RecipeRemainderHandler.REMAINDER_STACK.get()); + RecipeRemainderHandler.REMAINDER_STACK.remove(); + return remainder; + } +} diff --git a/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json b/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json index 69d431532..aeb3a0882 100644 --- a/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json +++ b/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json @@ -3,10 +3,13 @@ "package": "net.fabricmc.fabric.mixin.item", "compatibilityLevel": "JAVA_16", "mixins": [ - "ItemStackMixin", + "AbstractFurnaceBlockEntityMixin", + "ArmorItemMixin", + "BrewingStandBlockEntityMixin", "ItemMixin", + "ItemStackMixin", "LivingEntityMixin", - "ArmorItemMixin" + "RecipeMixin" ], "client": [ "client.ClientPlayerInteractionManagerMixin", diff --git a/fabric-item-api-v1/src/main/resources/fabric.mod.json b/fabric-item-api-v1/src/main/resources/fabric.mod.json index 7798a4d0d..cec709c7b 100644 --- a/fabric-item-api-v1/src/main/resources/fabric.mod.json +++ b/fabric-item-api-v1/src/main/resources/fabric.mod.json @@ -26,7 +26,8 @@ "custom": { "fabric-api:module-lifecycle": "stable", "loom:injected_interfaces": { - "net/minecraft/class_1792": ["net/fabricmc/fabric/api/item/v1/FabricItem"] + "net/minecraft/class_1792": ["net/fabricmc/fabric/api/item/v1/FabricItem"], + "net/minecraft/class_1799": ["net/fabricmc/fabric/api/item/v1/FabricItemStack"] } } } diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java index d7d83f4dd..ebbb628cd 100644 --- a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java +++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java @@ -16,10 +16,12 @@ package net.fabricmc.fabric.test.item; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.PickaxeItem; import net.minecraft.item.ToolMaterials; import net.minecraft.nbt.NbtCompound; +import net.minecraft.potion.Potions; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.registry.Registry; @@ -27,11 +29,17 @@ import net.minecraft.util.registry.Registry; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.item.v1.CustomDamageHandler; import net.fabricmc.fabric.api.item.v1.FabricItemSettings; +import net.fabricmc.fabric.api.registry.FuelRegistry; +import net.fabricmc.fabric.test.item.mixin.BrewingRecipeRegistryAccessor; public class CustomDamageTest implements ModInitializer { + public static final Item WEIRD_PICK = new WeirdPick(); + @Override public void onInitialize() { - Registry.register(Registry.ITEM, new Identifier("fabric-item-api-v1-testmod", "weird_pickaxe"), new WeirdPick()); + Registry.register(Registry.ITEM, new Identifier("fabric-item-api-v1-testmod", "weird_pickaxe"), WEIRD_PICK); + FuelRegistry.INSTANCE.add(WEIRD_PICK, 200); + BrewingRecipeRegistryAccessor.callRegisterPotionRecipe(Potions.WATER, WEIRD_PICK, Potions.AWKWARD); } public static final CustomDamageHandler WEIRD_DAMAGE_HANDLER = (stack, amount, entity, breakCallback) -> { @@ -55,5 +63,17 @@ public class CustomDamageTest implements ModInitializer { int v = stack.getOrCreateNbt().getInt("weird"); return super.getName(stack).shallowCopy().append(" (Weird Value: " + v + ")"); } + + @Override + public ItemStack getRecipeRemainder(ItemStack stack) { + if (stack.getDamage() < stack.getMaxDamage() - 1) { + ItemStack moreDamaged = stack.copy(); + moreDamaged.setCount(1); + moreDamaged.setDamage(stack.getDamage() + 1); + return moreDamaged; + } + + return ItemStack.EMPTY; + } } } diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java new file mode 100644 index 000000000..bf2cfc849 --- /dev/null +++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java @@ -0,0 +1,149 @@ +/* + * 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.item.gametest; + +import java.util.Objects; + +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.BrewingStandBlockEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.potion.PotionUtil; +import net.minecraft.potion.Potions; +import net.minecraft.test.GameTest; +import net.minecraft.test.TestContext; +import net.minecraft.util.math.BlockPos; + +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.fabricmc.fabric.test.item.CustomDamageTest; + +public class BrewingStandGameTest implements FabricGameTest { + private static final int BREWING_TIME = 800; + private static final BlockPos POS = new BlockPos(0, 1, 0); + + @GameTest(structureName = EMPTY_STRUCTURE) + public void basicBrewing(TestContext context) { + context.setBlockState(POS, Blocks.BREWING_STAND); + BrewingStandBlockEntity blockEntity = (BrewingStandBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS)); + + loadFuel(blockEntity, context); + + prepareForBrewing(blockEntity, new ItemStack(Items.NETHER_WART, 8), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER)); + + brew(blockEntity, context); + assertInventory(blockEntity, "Testing vanilla brewing.", + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + new ItemStack(Items.NETHER_WART, 7), + ItemStack.EMPTY); + + context.complete(); + } + + @GameTest(structureName = EMPTY_STRUCTURE) + public void vanillaRemainderTest(TestContext context) { + context.setBlockState(POS, Blocks.BREWING_STAND); + BrewingStandBlockEntity blockEntity = (BrewingStandBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS)); + + loadFuel(blockEntity, context); + + prepareForBrewing(blockEntity, new ItemStack(Items.DRAGON_BREATH), + PotionUtil.setPotion(new ItemStack(Items.SPLASH_POTION), Potions.AWKWARD)); + + brew(blockEntity, context); + assertInventory(blockEntity, "Testing vanilla brewing recipe remainder.", + PotionUtil.setPotion(new ItemStack(Items.LINGERING_POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.LINGERING_POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.LINGERING_POTION), Potions.AWKWARD), + new ItemStack(Items.GLASS_BOTTLE), + ItemStack.EMPTY); + + context.complete(); + } + + @GameTest(structureName = EMPTY_STRUCTURE) + public void fabricRemainderTest(TestContext context) { + context.setBlockState(POS, Blocks.BREWING_STAND); + BrewingStandBlockEntity blockEntity = (BrewingStandBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS)); + + loadFuel(blockEntity, context); + + prepareForBrewing(blockEntity, new ItemStack(CustomDamageTest.WEIRD_PICK), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER)); + + brew(blockEntity, context); + assertInventory(blockEntity, "Testing fabric brewing recipe remainder.", + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 1), + ItemStack.EMPTY); + + prepareForBrewing(blockEntity, RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 10), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER)); + + brew(blockEntity, context); + assertInventory(blockEntity, "Testing fabric brewing recipe remainder.", + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 11), + ItemStack.EMPTY); + + prepareForBrewing(blockEntity, RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 31), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER)); + + brew(blockEntity, context); + assertInventory(blockEntity, "Testing fabric brewing recipe remainder.", + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD), + ItemStack.EMPTY, + ItemStack.EMPTY); + + context.complete(); + } + + private void prepareForBrewing(BrewingStandBlockEntity blockEntity, ItemStack ingredient, ItemStack potion) { + blockEntity.setStack(0, potion.copy()); + blockEntity.setStack(1, potion.copy()); + blockEntity.setStack(2, potion.copy()); + blockEntity.setStack(3, ingredient); + } + + private void assertInventory(BrewingStandBlockEntity blockEntity, String extraErrorInfo, ItemStack... stacks) { + for (int i = 0; i < stacks.length; i++) { + ItemStack currentStack = blockEntity.getStack(i); + ItemStack expectedStack = stacks[i]; + + RecipeGameTest.assertStacks(currentStack, expectedStack, extraErrorInfo); + } + } + + private void loadFuel(BrewingStandBlockEntity blockEntity, TestContext context) { + blockEntity.setStack(4, new ItemStack(Items.BLAZE_POWDER)); + BrewingStandBlockEntity.tick(context.getWorld(), POS, context.getBlockState(POS), blockEntity); + } + + private void brew(BrewingStandBlockEntity blockEntity, TestContext context) { + for (int i = 0; i < BREWING_TIME; i++) { + BrewingStandBlockEntity.tick(context.getWorld(), POS, context.getBlockState(POS), blockEntity); + } + } +} diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java new file mode 100644 index 000000000..e9eac9f15 --- /dev/null +++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java @@ -0,0 +1,122 @@ +/* + * 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.item.gametest; + +import java.util.Objects; + +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.block.entity.FurnaceBlockEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.test.GameTest; +import net.minecraft.test.TestContext; +import net.minecraft.util.math.BlockPos; + +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.fabricmc.fabric.test.item.CustomDamageTest; + +public class FurnaceGameTest implements FabricGameTest { + private static final int COOK_TIME = 200; + private static final BlockPos POS = new BlockPos(0, 1, 0); + + @GameTest(structureName = EMPTY_STRUCTURE) + public void basicSmelt(TestContext context) { + context.setBlockState(POS, Blocks.FURNACE); + FurnaceBlockEntity blockEntity = (FurnaceBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS)); + + setInputs(blockEntity, new ItemStack(Blocks.COBBLESTONE, 8), new ItemStack(Items.COAL, 2)); + + cook(blockEntity, context, 1); + assertInventory(blockEntity, "Testing vanilla smelting.", + new ItemStack(Blocks.COBBLESTONE, 7), + new ItemStack(Items.COAL, 1), + new ItemStack(Blocks.STONE, 1)); + + cook(blockEntity, context, 7); + assertInventory(blockEntity, "Testing vanilla smelting.", + ItemStack.EMPTY, + new ItemStack(Items.COAL, 1), + new ItemStack(Blocks.STONE, 8)); + + context.complete(); + } + + @GameTest(structureName = EMPTY_STRUCTURE) + public void vanillaRemainderTest(TestContext context) { + context.setBlockState(POS, Blocks.FURNACE); + FurnaceBlockEntity blockEntity = (FurnaceBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS)); + + setInputs(blockEntity, new ItemStack(Blocks.COBBLESTONE, 64), new ItemStack(Items.LAVA_BUCKET)); + + cook(blockEntity, context, 64); + assertInventory(blockEntity, "Testing vanilla smelting recipe remainder.", + ItemStack.EMPTY, + new ItemStack(Items.BUCKET), + new ItemStack(Blocks.STONE, 64)); + + context.complete(); + } + + @GameTest(structureName = EMPTY_STRUCTURE) + public void fabricRemainderTest(TestContext context) { + context.setBlockState(POS, Blocks.FURNACE); + FurnaceBlockEntity blockEntity = (FurnaceBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS)); + + setInputs(blockEntity, new ItemStack(Blocks.COBBLESTONE, 32), new ItemStack(CustomDamageTest.WEIRD_PICK)); + + cook(blockEntity, context, 1); + assertInventory(blockEntity, "Testing fabric smelting recipe remainder.", + new ItemStack(Blocks.COBBLESTONE, 31), + RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 1), + new ItemStack(Blocks.STONE, 1)); + + cook(blockEntity, context, 30); + assertInventory(blockEntity, "Testing fabric smelting recipe remainder.", + new ItemStack(Blocks.COBBLESTONE, 1), + RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 31), + new ItemStack(Blocks.STONE, 31)); + + cook(blockEntity, context, 1); + assertInventory(blockEntity, "Testing fabric smelting recipe remainder.", + ItemStack.EMPTY, + ItemStack.EMPTY, + new ItemStack(Blocks.STONE, 32)); + + context.complete(); + } + + private void setInputs(FurnaceBlockEntity blockEntity, ItemStack ingredient, ItemStack fuel) { + blockEntity.setStack(0, ingredient); + blockEntity.setStack(1, fuel); + } + + private void assertInventory(FurnaceBlockEntity blockEntity, String extraErrorInfo, ItemStack... stacks) { + for (int i = 0; i < stacks.length; i++) { + ItemStack currentStack = blockEntity.getStack(i); + ItemStack expectedStack = stacks[i]; + + RecipeGameTest.assertStacks(currentStack, expectedStack, extraErrorInfo); + } + } + + private void cook(FurnaceBlockEntity blockEntity, TestContext context, int items) { + for (int i = 0; i < COOK_TIME * items; i++) { + AbstractFurnaceBlockEntity.tick(context.getWorld(), POS, context.getBlockState(POS), blockEntity); + } + } +} diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java new file mode 100644 index 000000000..53b03e9ea --- /dev/null +++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java @@ -0,0 +1,144 @@ +/* + * 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.item.gametest; + +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.recipe.Recipe; +import net.minecraft.recipe.RecipeSerializer; +import net.minecraft.recipe.RecipeType; +import net.minecraft.test.GameTest; +import net.minecraft.test.GameTestException; +import net.minecraft.test.TestContext; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.world.World; + +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; +import net.fabricmc.fabric.test.item.CustomDamageTest; + +public class RecipeGameTest implements FabricGameTest { + @GameTest(structureName = EMPTY_STRUCTURE) + public void vanillaRemainderTest(TestContext context) { + Recipe<SimpleInventory> testRecipe = createTestingRecipeInstance(); + + SimpleInventory inventory = new SimpleInventory( + new ItemStack(Items.WATER_BUCKET), + new ItemStack(Items.DIAMOND)); + + DefaultedList<ItemStack> remainderList = testRecipe.getRemainder(inventory); + + assertStackList(remainderList, "Testing vanilla recipe remainder.", + new ItemStack(Items.BUCKET), + ItemStack.EMPTY); + + context.complete(); + } + + @GameTest(structureName = EMPTY_STRUCTURE) + public void fabricRemainderTest(TestContext context) { + Recipe<SimpleInventory> testRecipe = createTestingRecipeInstance(); + + SimpleInventory inventory = new SimpleInventory( + new ItemStack(CustomDamageTest.WEIRD_PICK), + withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 10), + withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 31), + new ItemStack(Items.DIAMOND)); + + DefaultedList<ItemStack> remainderList = testRecipe.getRemainder(inventory); + + assertStackList(remainderList, "Testing fabric recipe remainder.", + withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 1), + withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 11), + ItemStack.EMPTY, + ItemStack.EMPTY); + + context.complete(); + } + + private Recipe<SimpleInventory> createTestingRecipeInstance() { + return new Recipe<>() { + @Override + public boolean matches(SimpleInventory inventory, World world) { + return true; + } + + @Override + public ItemStack craft(SimpleInventory inventory) { + return null; + } + + @Override + public boolean fits(int width, int height) { + return true; + } + + @Override + public ItemStack getOutput() { + return null; + } + + @Override + public Identifier getId() { + return null; + } + + @Override + public RecipeSerializer<?> getSerializer() { + return null; + } + + @Override + public RecipeType<?> getType() { + return null; + } + }; + } + + private void assertStackList(DefaultedList<ItemStack> stackList, String extraErrorInfo, ItemStack... stacks) { + for (int i = 0; i < stackList.size(); i++) { + ItemStack currentStack = stackList.get(i); + ItemStack expectedStack = stacks[i]; + + assertStacks(currentStack, expectedStack, extraErrorInfo); + } + } + + static void assertStacks(ItemStack currentStack, ItemStack expectedStack, String extraErrorInfo) { + if (currentStack.isEmpty() && expectedStack.isEmpty()) { + return; + } + + if (!currentStack.isItemEqual(expectedStack)) { + throw new GameTestException("Item stacks dont match. " + extraErrorInfo); + } + + if (currentStack.getCount() != expectedStack.getCount()) { + throw new GameTestException("Size doesnt match. " + extraErrorInfo); + } + + if (!ItemStack.areNbtEqual(currentStack, expectedStack)) { + throw new GameTestException("Nbt doesnt match. " + extraErrorInfo); + } + } + + static ItemStack withDamage(ItemStack stack, int damage) { + stack.setDamage(damage); + return stack; + } +} diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java new file mode 100644 index 000000000..2bdaffced --- /dev/null +++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java @@ -0,0 +1,32 @@ +/* + * 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.item.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.item.Item; +import net.minecraft.potion.Potion; +import net.minecraft.recipe.BrewingRecipeRegistry; + +@Mixin(BrewingRecipeRegistry.class) +public interface BrewingRecipeRegistryAccessor { + @Invoker + static void callRegisterPotionRecipe(Potion input, Item item, Potion output) { + throw new UnsupportedOperationException(); + } +} diff --git a/fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json b/fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json new file mode 100644 index 000000000..c89f4aa3b --- /dev/null +++ b/fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { + "item": "minecraft:diamond_ore" + }, + { + "item": "fabric-item-api-v1-testmod:weird_pickaxe" + } + ], + "result": { + "item": "minecraft:diamond", + "count": 1 + } +} diff --git a/fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json b/fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json new file mode 100644 index 000000000..6a2e72344 --- /dev/null +++ b/fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.test.item.mixin", + "compatibilityLevel": "JAVA_16", + "mixins": [ + "BrewingRecipeRegistryAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-item-api-v1/src/testmod/resources/fabric.mod.json b/fabric-item-api-v1/src/testmod/resources/fabric.mod.json index 93e8e9873..56e2f60a6 100644 --- a/fabric-item-api-v1/src/testmod/resources/fabric.mod.json +++ b/fabric-item-api-v1/src/testmod/resources/fabric.mod.json @@ -18,6 +18,14 @@ ], "client": [ "net.fabricmc.fabric.test.item.client.TooltipTests" + ], + "fabric-gametest" : [ + "net.fabricmc.fabric.test.item.gametest.BrewingStandGameTest", + "net.fabricmc.fabric.test.item.gametest.FurnaceGameTest", + "net.fabricmc.fabric.test.item.gametest.RecipeGameTest" ] - } + }, + "mixins": [ + "fabric-item-api-tests-v1.mixins.json" + ] }