Replace some recipes with a more dynamic system

This adds a new "recipe function" system, that allows transforming the
result of a recipe according to some datapack-defined function.

Currently, we only provide one function: computercraft:copy_components,
which copies components from one of the ingredients to the result. This
allows us to replace several of our existing recipes:

 - Turtle overlay recipes are now defined as a normal shapeless recipe
   that copies all (non-overlay) components from the input turtle.

 - Computer conversion recipes (e.g. computer -> turtle, normal ->
   advanced) copy all components from the input computer to the result.

This is more complex (and thus more code), but also a little more
flexible, which hopefully is useful for someone :).
This commit is contained in:
Jonathan Coates 2024-04-26 19:45:09 +01:00
parent d7786ee4b9
commit a3b07909b0
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
20 changed files with 489 additions and 156 deletions

View File

@ -1,6 +1,7 @@
{
"type": "computercraft:computer_convert",
"type": "computercraft:transform_shaped",
"category": "redstone",
"function": [{"type": "computercraft:copy_components", "from": {"item": "computercraft:computer_normal"}}],
"key": {"#": {"tag": "c:ingots/gold"}, "C": {"item": "computercraft:computer_normal"}},
"pattern": ["###", "#C#", "# #"],
"result": {"count": 1, "id": "computercraft:computer_advanced"}

View File

@ -1,6 +1,9 @@
{
"type": "computercraft:computer_convert",
"type": "computercraft:transform_shaped",
"category": "redstone",
"function": [
{"type": "computercraft:copy_components", "from": {"item": "computercraft:pocket_computer_normal"}}
],
"key": {"#": {"tag": "c:ingots/gold"}, "C": {"item": "computercraft:pocket_computer_normal"}},
"pattern": ["###", "#C#", "# #"],
"result": {"count": 1, "id": "computercraft:pocket_computer_advanced"}

View File

@ -1,6 +1,7 @@
{
"type": "computercraft:computer_convert",
"type": "computercraft:transform_shaped",
"category": "redstone",
"function": [{"type": "computercraft:copy_components", "from": {"item": "computercraft:computer_advanced"}}],
"key": {
"#": {"tag": "c:ingots/gold"},
"C": {"item": "computercraft:computer_advanced"},

View File

@ -1,6 +1,13 @@
{
"type": "computercraft:turtle_overlay",
"type": "computercraft:transform_shapeless",
"category": "redstone",
"function": [
{
"type": "computercraft:copy_components",
"exclude": ["computercraft:overlay"],
"from": {"item": "computercraft:turtle_advanced"}
}
],
"group": "computercraft:turtle_advanced_overlay",
"ingredients": [
{"tag": "c:dyes/red"},
@ -12,6 +19,9 @@
{"item": "minecraft:stick"},
{"item": "computercraft:turtle_advanced"}
],
"overlay": "computercraft:block/turtle_rainbow_overlay",
"result": {"count": 1, "id": "computercraft:turtle_advanced"}
"result": {
"components": {"computercraft:overlay": "computercraft:block/turtle_rainbow_overlay"},
"count": 1,
"id": "computercraft:turtle_advanced"
}
}

View File

@ -1,6 +1,13 @@
{
"type": "computercraft:turtle_overlay",
"type": "computercraft:transform_shapeless",
"category": "redstone",
"function": [
{
"type": "computercraft:copy_components",
"exclude": ["computercraft:overlay"],
"from": {"item": "computercraft:turtle_advanced"}
}
],
"group": "computercraft:turtle_advanced_overlay",
"ingredients": [
{"tag": "c:dyes/light_blue"},
@ -9,6 +16,9 @@
{"item": "minecraft:stick"},
{"item": "computercraft:turtle_advanced"}
],
"overlay": "computercraft:block/turtle_trans_overlay",
"result": {"count": 1, "id": "computercraft:turtle_advanced"}
"result": {
"components": {"computercraft:overlay": "computercraft:block/turtle_trans_overlay"},
"count": 1,
"id": "computercraft:turtle_advanced"
}
}

View File

@ -1,6 +1,7 @@
{
"type": "computercraft:computer_convert",
"type": "computercraft:transform_shaped",
"category": "redstone",
"function": [{"type": "computercraft:copy_components", "from": {"item": "computercraft:turtle_normal"}}],
"key": {
"#": {"tag": "c:ingots/gold"},
"B": {"tag": "c:storage_blocks/gold"},

View File

@ -1,6 +1,7 @@
{
"type": "computercraft:computer_convert",
"type": "computercraft:transform_shaped",
"category": "redstone",
"function": [{"type": "computercraft:copy_components", "from": {"item": "computercraft:computer_normal"}}],
"key": {
"#": {"tag": "c:ingots/iron"},
"C": {"item": "computercraft:computer_normal"},

View File

@ -1,6 +1,13 @@
{
"type": "computercraft:turtle_overlay",
"type": "computercraft:transform_shapeless",
"category": "redstone",
"function": [
{
"type": "computercraft:copy_components",
"exclude": ["computercraft:overlay"],
"from": {"item": "computercraft:turtle_normal"}
}
],
"group": "computercraft:turtle_normal_overlay",
"ingredients": [
{"tag": "c:dyes/red"},
@ -12,6 +19,9 @@
{"item": "minecraft:stick"},
{"item": "computercraft:turtle_normal"}
],
"overlay": "computercraft:block/turtle_rainbow_overlay",
"result": {"count": 1, "id": "computercraft:turtle_normal"}
"result": {
"components": {"computercraft:overlay": "computercraft:block/turtle_rainbow_overlay"},
"count": 1,
"id": "computercraft:turtle_normal"
}
}

View File

@ -1,6 +1,13 @@
{
"type": "computercraft:turtle_overlay",
"type": "computercraft:transform_shapeless",
"category": "redstone",
"function": [
{
"type": "computercraft:copy_components",
"exclude": ["computercraft:overlay"],
"from": {"item": "computercraft:turtle_normal"}
}
],
"group": "computercraft:turtle_normal_overlay",
"ingredients": [
{"tag": "c:dyes/light_blue"},
@ -9,6 +16,9 @@
{"item": "minecraft:stick"},
{"item": "computercraft:turtle_normal"}
],
"overlay": "computercraft:block/turtle_trans_overlay",
"result": {"count": 1, "id": "computercraft:turtle_normal"}
"result": {
"components": {"computercraft:overlay": "computercraft:block/turtle_trans_overlay"},
"count": 1,
"id": "computercraft:turtle_normal"
}
}

View File

@ -18,7 +18,6 @@
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.common.ClearColourRecipe;
import dan200.computercraft.shared.common.ColourableRecipe;
import dan200.computercraft.shared.computer.recipe.ComputerConvertRecipe;
import dan200.computercraft.shared.media.recipes.DiskRecipe;
import dan200.computercraft.shared.media.recipes.PrintoutRecipe;
import dan200.computercraft.shared.platform.PlatformHelper;
@ -27,8 +26,10 @@
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
import dan200.computercraft.shared.recipe.TransformShapedRecipe;
import dan200.computercraft.shared.recipe.TransformShapelessRecipe;
import dan200.computercraft.shared.recipe.function.CopyComponents;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.turtle.recipes.TurtleOverlayRecipe;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
import dan200.computercraft.shared.util.ColourUtils;
import dan200.computercraft.shared.util.DataComponentUtil;
@ -194,16 +195,21 @@ private void turtleOverlays(RecipeOutput add) {
}
private void turtleOverlay(RecipeOutput add, String overlay, Consumer<ShapelessSpecBuilder> build) {
var overlayId = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay);
for (var turtleItem : turtleItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
var builder = ShapelessSpecBuilder.shapeless(RecipeCategory.REDSTONE, new ItemStack(turtleItem))
var builder = ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), overlayId))
.group(name.withSuffix("_overlay").toString())
.unlockedBy("has_turtle", inventoryChange(turtleItem));
build.accept(builder);
builder
.requires(turtleItem)
.build(s -> new TurtleOverlayRecipe(s, new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay)))
.build(s -> new TransformShapelessRecipe(s, List.of(
CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
)))
.save(add, name.withSuffix("_overlays/" + overlay));
}
}
@ -251,7 +257,7 @@ private void basicRecipes(RecipeOutput add) {
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(ComputerConvertRecipe::new)
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
.save(add, new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
ShapedRecipeBuilder
@ -274,7 +280,7 @@ private void basicRecipes(RecipeOutput add) {
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.build(ComputerConvertRecipe::new)
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
.save(add);
ShapedSpecBuilder
@ -286,7 +292,7 @@ private void basicRecipes(RecipeOutput add) {
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.build(ComputerConvertRecipe::new)
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_ADVANCED.get()))))
.save(add);
ShapedSpecBuilder
@ -298,7 +304,7 @@ private void basicRecipes(RecipeOutput add) {
.define('C', ModRegistry.Items.TURTLE_NORMAL.get())
.define('B', ingredients.goldBlock())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(ComputerConvertRecipe::new)
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.TURTLE_NORMAL.get()))))
.save(add, new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
ShapedRecipeBuilder
@ -363,7 +369,7 @@ private void basicRecipes(RecipeOutput add) {
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(ComputerConvertRecipe::new)
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()))))
.save(add, new ResourceLocation(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
ShapedRecipeBuilder

View File

@ -37,7 +37,6 @@
import dan200.computercraft.shared.computer.items.CommandComputerItem;
import dan200.computercraft.shared.computer.items.ComputerItem;
import dan200.computercraft.shared.computer.items.ServerComputerReference;
import dan200.computercraft.shared.computer.recipe.ComputerConvertRecipe;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
@ -71,16 +70,14 @@
import dan200.computercraft.shared.pocket.peripherals.PocketModem;
import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
import dan200.computercraft.shared.recipe.*;
import dan200.computercraft.shared.recipe.function.CopyComponents;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.turtle.recipes.TurtleOverlayRecipe;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
import dan200.computercraft.shared.turtle.upgrades.*;
import dan200.computercraft.shared.util.DataComponentUtil;
@ -91,14 +88,17 @@
import net.minecraft.core.cauldron.CauldronInteraction;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.*;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
import net.minecraft.world.level.block.Block;
@ -474,17 +474,32 @@ private static <T extends CustomRecipe> RegistryEntry<SimpleCraftingRecipeSerial
return REGISTRY.register(name, () -> new SimpleCraftingRecipeSerializer<>(factory));
}
private static <T extends Recipe<?>> RegistryEntry<RecipeSerializer<T>> register(String name, MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> streamCodec) {
return REGISTRY.register(name, () -> new BasicRecipeSerialiser<>(codec, streamCodec));
}
public static final RegistryEntry<RecipeSerializer<ImpostorShapedRecipe>> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", () -> CustomShapedRecipe.serialiser(ImpostorShapedRecipe::new));
public static final RegistryEntry<RecipeSerializer<ImpostorShapelessRecipe>> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", () -> CustomShapelessRecipe.serialiser(ImpostorShapelessRecipe::new));
public static final RegistryEntry<RecipeSerializer<TransformShapedRecipe>> TRANSFORM_SHAPED = register("transform_shaped", TransformShapedRecipe.CODEC, TransformShapedRecipe.STREAM_CODEC);
public static final RegistryEntry<RecipeSerializer<TransformShapelessRecipe>> TRANSFORM_SHAPELESS = register("transform_shapeless", TransformShapelessRecipe.CODEC, TransformShapelessRecipe.STREAM_CODEC);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<ColourableRecipe>> DYEABLE_ITEM = simple("colour", ColourableRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<ClearColourRecipe>> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe::serialiser);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PocketComputerUpgradeRecipe>> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::new);
public static final RegistryEntry<RecipeSerializer<ComputerConvertRecipe>> COMPUTER_CONVERT = REGISTRY.register("computer_convert", () -> CustomShapedRecipe.serialiser(ComputerConvertRecipe::new));
}
public static class RecipeFunctions {
static final RegistrationHelper<RecipeFunction.Type<?>> REGISTRY = PlatformHelper.get().createRegistrationHelper(RecipeFunction.REGISTRY);
private static <T extends RecipeFunction> RegistryEntry<RecipeFunction.Type<T>> register(String name, MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> streamCodec) {
return REGISTRY.register(name, () -> new RecipeFunction.Type<>(codec, streamCodec));
}
public static final RegistryEntry<RecipeFunction.Type<CopyComponents>> COPY_COMPONENTS = register("copy_components", CopyComponents.CODEC, CopyComponents.STREAM_CODEC);
}
public static class Permissions {
@ -553,6 +568,7 @@ public static void register() {
ArgumentTypes.REGISTRY.register();
LootItemConditionTypes.REGISTRY.register();
RecipeSerializers.REGISTRY.register();
RecipeFunctions.REGISTRY.register();
Permissions.REGISTRY.register();
CreativeTabs.REGISTRY.register();

View File

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.computer.recipe;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.items.AbstractComputerItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
import net.minecraft.core.HolderLookup;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
/**
* A recipe which converts a computer from one form into another.
*/
public final class ComputerConvertRecipe extends CustomShapedRecipe {
private final Item result;
public ComputerConvertRecipe(ShapedRecipeSpec recipe) {
super(recipe);
this.result = recipe.result().getItem();
}
@Override
public ItemStack assemble(CraftingContainer inventory, HolderLookup.Provider registryAccess) {
// Find our computer item and copy the components across.
for (var i = 0; i < inventory.getContainerSize(); i++) {
var stack = inventory.getItem(i);
if (isComputerItem(stack.getItem())) {
var newStack = new ItemStack(result);
newStack.applyComponents(stack.getComponentsPatch());
return newStack;
}
}
return ItemStack.EMPTY;
}
@Override
public RecipeSerializer<ComputerConvertRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.COMPUTER_CONVERT.get();
}
private static boolean isComputerItem(Item item) {
// TODO: Make this a little more general. Either with a tag, or a predicate on the recipe itself?
return item instanceof AbstractComputerItem || item instanceof PocketComputerItem;
}
}

View File

@ -32,7 +32,7 @@ public final ShapedRecipeSpec toSpec() {
public abstract RecipeSerializer<? extends CustomShapedRecipe> getSerializer();
public static <T extends CustomShapedRecipe> RecipeSerializer<T> serialiser(Function<ShapedRecipeSpec, T> factory) {
return new BasicRecipeSerialiser<T>(
return new BasicRecipeSerialiser<>(
ShapedRecipeSpec.CODEC.xmap(factory, CustomShapedRecipe::toSpec),
ShapedRecipeSpec.STREAM_CODEC.map(factory, CustomShapedRecipe::toSpec)
);

View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import net.minecraft.core.HolderLookup;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapedRecipe;
import java.util.List;
/**
* A {@link ShapedRecipe} that applies a list of {@linkplain RecipeFunction recipe functions}.
*/
public final class TransformShapedRecipe extends CustomShapedRecipe {
public static final MapCodec<TransformShapedRecipe> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ShapedRecipeSpec.CODEC.forGetter(TransformShapedRecipe::toSpec),
RecipeFunction.LIST_CODEC.fieldOf("function").forGetter(x -> x.functions)
).apply(instance, TransformShapedRecipe::new)
);
public static final StreamCodec<RegistryFriendlyByteBuf, TransformShapedRecipe> STREAM_CODEC = StreamCodec.composite(
ShapedRecipeSpec.STREAM_CODEC, TransformShapedRecipe::toSpec,
RecipeFunction.LIST_STREAM_CODEC, x -> x.functions,
TransformShapedRecipe::new
);
private final List<RecipeFunction> functions;
public TransformShapedRecipe(ShapedRecipeSpec recipe, List<RecipeFunction> functions) {
super(recipe);
this.functions = functions;
}
@Override
public ItemStack assemble(CraftingContainer inventory, HolderLookup.Provider registryAccess) {
var result = super.assemble(inventory, registryAccess);
for (var function : functions) result = function.apply(inventory, result);
return result;
}
@Override
public RecipeSerializer<TransformShapedRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.TRANSFORM_SHAPED.get();
}
}

View File

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import net.minecraft.core.HolderLookup;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapelessRecipe;
import java.util.List;
/**
* A {@link ShapelessRecipe} that applies a list of {@linkplain RecipeFunction recipe functions}.
*/
public class TransformShapelessRecipe extends CustomShapelessRecipe {
public static final MapCodec<TransformShapelessRecipe> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ShapelessRecipeSpec.CODEC.forGetter(TransformShapelessRecipe::toSpec),
RecipeFunction.LIST_CODEC.fieldOf("function").forGetter(x -> x.functions)
).apply(instance, TransformShapelessRecipe::new));
public static final StreamCodec<RegistryFriendlyByteBuf, TransformShapelessRecipe> STREAM_CODEC = StreamCodec.composite(
ShapelessRecipeSpec.STREAM_CODEC, CustomShapelessRecipe::toSpec,
RecipeFunction.LIST_STREAM_CODEC, x -> x.functions,
TransformShapelessRecipe::new
);
private final List<RecipeFunction> functions;
public TransformShapelessRecipe(ShapelessRecipeSpec spec, List<RecipeFunction> functions) {
super(spec);
this.functions = functions;
}
@Override
public ItemStack assemble(CraftingContainer inventory, HolderLookup.Provider registryAccess) {
var result = super.assemble(inventory, registryAccess);
for (var function : functions) result = function.apply(inventory, result);
return result;
}
@Override
public RecipeSerializer<TransformShapelessRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.TRANSFORM_SHAPELESS.get();
}
}

View File

@ -0,0 +1,179 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe.function;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.storage.loot.functions.CopyComponentsFunction;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* A {@link RecipeFunction} that copies components from one of the ingredients to the result.
* <p>
* This has the same behaviour as {@linkplain CopyComponentsFunction}, but operating within the scope of recipes.
*/
public final class CopyComponents implements RecipeFunction {
public static final MapCodec<CopyComponents> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Ingredient.CODEC_NONEMPTY.fieldOf("from").forGetter(x -> x.from),
DataComponentType.CODEC.listOf().optionalFieldOf("include").forGetter(x -> x.include),
DataComponentType.CODEC.listOf().optionalFieldOf("exclude").forGetter(x -> x.exclude)
).apply(instance, CopyComponents::new));
public static final StreamCodec<RegistryFriendlyByteBuf, CopyComponents> STREAM_CODEC = StreamCodec.composite(
Ingredient.CONTENTS_STREAM_CODEC, x -> x.from,
ByteBufCodecs.optional(DataComponentType.STREAM_CODEC.apply(ByteBufCodecs.list())), x -> x.include,
ByteBufCodecs.optional(DataComponentType.STREAM_CODEC.apply(ByteBufCodecs.list())), x -> x.exclude,
CopyComponents::new
);
private final Ingredient from;
private final Optional<List<DataComponentType<?>>> include;
private final Optional<List<DataComponentType<?>>> exclude;
private final @Nullable Set<DataComponentType<?>> includeSet;
private final @Nullable Set<DataComponentType<?>> excludeSet;
/**
* Create a new {@link CopyComponents} that copies all components from an ingredient.
*
* @param from The ingredient to copy from.
*/
public CopyComponents(Ingredient from) {
this(from, Optional.empty(), Optional.empty());
}
/**
* Create a new {@link CopyComponents} that copies all components from an ingredient.
*
* @param from The ingredient to copy from.
*/
public CopyComponents(ItemLike from) {
this(Ingredient.of(from));
}
private CopyComponents(Ingredient from, Optional<List<DataComponentType<?>>> include, Optional<List<DataComponentType<?>>> exclude) {
this.from = from;
this.include = include.map(List::copyOf);
this.exclude = exclude.map(List::copyOf);
includeSet = include.map(Set::copyOf).orElse(null);
excludeSet = exclude.map(Set::copyOf).orElse(null);
}
@Override
public Type<?> getType() {
return ModRegistry.RecipeFunctions.COPY_COMPONENTS.get();
}
@Override
public ItemStack apply(CraftingContainer container, ItemStack result) {
for (var item : container.getItems()) {
if (from.test(item)) {
applyPatch(item.getComponentsPatch(), result);
break;
}
}
return result;
}
private void applyPatch(DataComponentPatch patch, ItemStack result) {
if (includeSet == null && excludeSet == null) {
result.applyComponents(patch);
return;
}
// Only apply components in the include set (if present) and not in the exclude set (if present).
for (var component : patch.entrySet()) {
var type = component.getKey();
if ((includeSet == null || includeSet.contains(type)) && (excludeSet == null || !excludeSet.contains(type))) {
unsafeSetComponent(result, type, component.getValue().orElse(null));
}
}
}
@SuppressWarnings("unchecked")
private static <T> void unsafeSetComponent(ItemStack stack, DataComponentType<?> type, @Nullable T value) {
stack.set((DataComponentType<T>) type, value);
}
/**
* Create a new {@link CopyComponents} builder, that copies components from an ingredient.
*
* @param ingredient The ingredient to copy from.
* @return The builder.
*/
public static Builder builder(Ingredient ingredient) {
return new Builder(ingredient);
}
/**
* Create a new {@link CopyComponents} builder, that copies components from an ingredient.
*
* @param ingredient The ingredient to copy from.
* @return The builder.
*/
public static Builder builder(ItemLike ingredient) {
return new Builder(Ingredient.of(ingredient));
}
public static final class Builder {
private final Ingredient from;
private @Nullable List<DataComponentType<?>> include;
private @Nullable List<DataComponentType<?>> exclude;
private Builder(Ingredient from) {
this.from = from;
}
/**
* Only copy the specified component.
*
* @param type The component to include.
* @return {@code this}, for chaining.
*/
public Builder include(DataComponentType<?> type) {
if (this.include == null) include = new ArrayList<>();
include.add(type);
return this;
}
/**
* Exclude a component from being copied.
*
* @param type The component to exclude.
* @return {@code this}, for chaining.
*/
public Builder exclude(DataComponentType<?> type) {
if (exclude == null) exclude = new ArrayList<>();
exclude.add(type);
return this;
}
/**
* Build the resulting {@link CopyComponents} instance.
*
* @return The constructed {@link CopyComponents} recipe function.
*/
public CopyComponents build() {
return new CopyComponents(from, Optional.ofNullable(include), Optional.ofNullable(exclude));
}
}
}

View File

@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe.function;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.impl.RegistryHelper;
import dan200.computercraft.shared.recipe.TransformShapedRecipe;
import dan200.computercraft.shared.recipe.TransformShapelessRecipe;
import net.minecraft.core.Registry;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.functions.LootItemFunction;
import java.util.List;
/**
* A function that is applied to the result of a recipe, mutating it in some way. These can be used from within a recipe
* JSON file to define basic dynamic recipes, rather than having to fall back to Java.
* <p>
* For instance, the recipe to convert a normal computer to a turtle, is defined as a basic shaped recipes plus an
* additional {@link CopyComponents} function, that copies the id and label from the computer to the turtle.
* <p>
* The design and implementation of these are very similar to Minecraft's existing {@linkplain LootItemFunction loot
* functions}.
*
* @see TransformShapedRecipe
* @see TransformShapelessRecipe
*/
public interface RecipeFunction {
/**
* The registry where {@link RecipeFunction}s are registered.
*/
ResourceKey<Registry<Type<?>>> REGISTRY = ResourceKey.createRegistryKey(new ResourceLocation(ComputerCraftAPI.MOD_ID, "recipe_function"));
/**
* The codec to read and write {@link RecipeFunction}s with.
*/
Codec<RecipeFunction> CODEC = Codec.lazyInitialized(() -> RegistryHelper.getRegistry(REGISTRY).byNameCodec().dispatch(RecipeFunction::getType, Type::codec));
/**
* A codec for a list of functions.
*/
Codec<List<RecipeFunction>> LIST_CODEC = CODEC.listOf(1, Integer.MAX_VALUE);
/**
* The {@link StreamCodec} equivalent of {@link #CODEC}.
*/
StreamCodec<RegistryFriendlyByteBuf, RecipeFunction> STREAM_CODEC = ByteBufCodecs.registry(REGISTRY).dispatch(RecipeFunction::getType, Type::streamCodec);
/**
* The {@link StreamCodec} equivalent of {@link #LIST_CODEC}.
*/
StreamCodec<RegistryFriendlyByteBuf, List<RecipeFunction>> LIST_STREAM_CODEC = STREAM_CODEC.apply(ByteBufCodecs.list());
/**
* Get the type of this recipe function.
*
* @return The type of this recipe function.
*/
Type<?> getType();
/**
* Apply this recipe function, modifying the result item.
*
* @param container The current crafting container.
* @param result The result item to modify. This may be mutated in place.
* @return The new result item. This may be {@code result}.
*/
ItemStack apply(CraftingContainer container, ItemStack result);
/**
* Properties about a type of {@link RecipeFunction}. These are stored in {@linkplain #REGISTRY a Minecraft
* registry}, and returned by {@link #getType()}.
*
* @param codec The codec to read and write this class of recipe functions with.
* @param streamCodec The network codec to read and write this class of recipe functions with.
* @param <T> The type of recipe function.
*/
record Type<T extends RecipeFunction>(MapCodec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> streamCodec) {
}
}

View File

@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.turtle.recipes;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.recipe.BasicRecipeSerialiser;
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
import dan200.computercraft.shared.recipe.ShapelessRecipeSpec;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.util.DataComponentUtil;
import net.minecraft.core.HolderLookup;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapelessRecipe;
/**
* A {@link ShapelessRecipe} which sets the {@linkplain TurtleItem#getOverlay(ItemStack)} turtle's overlay} instead.
*/
public class TurtleOverlayRecipe extends CustomShapelessRecipe {
private static final MapCodec<TurtleOverlayRecipe> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ShapelessRecipeSpec.CODEC.forGetter(CustomShapelessRecipe::toSpec),
ResourceLocation.CODEC.fieldOf("overlay").forGetter(x -> x.overlay)
).apply(instance, TurtleOverlayRecipe::new));
private static final StreamCodec<RegistryFriendlyByteBuf, TurtleOverlayRecipe> STREAM_CODEC = StreamCodec.composite(
ShapelessRecipeSpec.STREAM_CODEC, CustomShapelessRecipe::toSpec,
ResourceLocation.STREAM_CODEC, x -> x.overlay,
TurtleOverlayRecipe::new
);
private final ResourceLocation overlay;
public TurtleOverlayRecipe(ShapelessRecipeSpec spec, ResourceLocation overlay) {
super(spec);
this.overlay = overlay;
}
private static ItemStack make(ItemStack stack, ResourceLocation overlay) {
return DataComponentUtil.createResult(stack, ModRegistry.DataComponents.OVERLAY.get(), overlay);
}
@Override
public ItemStack assemble(CraftingContainer inventory, HolderLookup.Provider registryAccess) {
for (var i = 0; i < inventory.getContainerSize(); i++) {
var stack = inventory.getItem(i);
if (stack.getItem() instanceof TurtleItem) return make(stack, overlay);
}
return ItemStack.EMPTY;
}
@Override
public RecipeSerializer<TurtleOverlayRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
}
public static RecipeSerializer<TurtleOverlayRecipe> serialiser() {
return new BasicRecipeSerialiser<>(CODEC, STREAM_CODEC);
}
}

View File

@ -22,12 +22,15 @@
import dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlockEntity;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockEntity;
import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;
import net.fabricmc.fabric.api.loot.v2.LootTableEvents;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
@ -60,6 +63,8 @@ public static void init() {
ServerPlayNetworking.registerGlobalReceiver(type.type(), (packet, player) -> packet.handle(player::player));
}
FabricRegistryBuilder.createSimple(RecipeFunction.REGISTRY).attribute(RegistryAttribute.SYNCED).buildAndRegister();
ModRegistry.register();
ModRegistry.registerMainThread();

View File

@ -30,6 +30,7 @@
import dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlockEntity;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockEntity;
import dan200.computercraft.shared.platform.ForgeConfigFile;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.server.level.ServerPlayer;
@ -81,6 +82,7 @@ public static IEventBus getEventBus() {
public static void registerRegistries(NewRegistryEvent event) {
event.create(new RegistryBuilder<>(ITurtleUpgrade.serialiserRegistryKey()));
event.create(new RegistryBuilder<>(IPocketUpgrade.serialiserRegistryKey()));
event.create(new RegistryBuilder<>(RecipeFunction.REGISTRY).sync(true));
}
@SubscribeEvent