Try to make recipe serialisers more reusable

This attempts to reduce some duplication in recipe serialisation (and
deserialisation) by moving the structure of a recipe (group, category,
ingredients, result) into seprate types.

 - Add ShapedRecipeSpec and ShapelessRecipeSpec, which store the core
   properties of shaped and shapeless recipes. There's a couple of
   additional classes here for handling some of the other shared or
   complex logic.

 - These classes are now used by two new Custom{Shaped,Shapeless}Recipe
   classes, which are (mostly) equivalent to Minecraft's
   shaped/shapeless recipes, just with support for nbt in results.

 - All the other similar recipes now inherit from these base classes,
   which allows us to reuse a lot of this serialisation code. Alas, the
   total code size has still gone up - maybe there's too much
   abstraction here :).

 - Mostly unrelated, but fix the skull recipes using the wrong UUID
   format.

This allows us to remove our mixin for nbt in recipes (as we just use
our custom recipe now) and simplify serialisation a bit - hopefully
making the switch to codecs a little easier.
This commit is contained in:
Jonathan Coates 2023-09-23 18:24:02 +01:00
parent 0d6c6e7ae7
commit e6125bcf60
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
39 changed files with 728 additions and 558 deletions

View File

@ -5,6 +5,7 @@
package dan200.computercraft.data;
import com.google.gson.JsonObject;
import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
@ -25,15 +26,12 @@
import net.minecraft.data.PackOutput;
import net.minecraft.data.recipes.*;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.DyeItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapedRecipe;
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
import net.minecraft.world.level.ItemLike;
@ -41,6 +39,7 @@
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Consumer;
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
@ -443,7 +442,7 @@ private void basicRecipes(Consumer<FinishedRecipe> add) {
.requires(ModRegistry.Items.MONITOR_NORMAL.get())
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
.save(
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
.withResultTag(playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c")),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_cloudy")
);
@ -454,7 +453,7 @@ private void basicRecipes(Consumer<FinishedRecipe> add) {
.requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
.save(
RecipeWrapper.wrap(RecipeSerializer.SHAPELESS_RECIPE, add)
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.SHAPELESS.get(), add)
.withResultTag(playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb")),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_dan200")
);
@ -513,17 +512,15 @@ private static ItemPredicate itemPredicate(Ingredient ingredient) {
}
private static CompoundTag playerHead(String name, String uuid) {
var owner = new CompoundTag();
owner.putString("Name", name);
owner.putString("Id", uuid);
var owner = NbtUtils.writeGameProfile(new CompoundTag(), new GameProfile(UUID.fromString(uuid), name));
var tag = new CompoundTag();
tag.put("SkullOwner", owner);
tag.put(PlayerHeadItem.TAG_SKULL_OWNER, owner);
return tag;
}
private static Consumer<JsonObject> family(ComputerFamily family) {
return json -> json.addProperty("family", family.toString());
return json -> json.addProperty("family", family.getSerializedName());
}
private static void addSpecial(Consumer<FinishedRecipe> add, SimpleCraftingRecipeSerializer<?> special) {

View File

@ -70,6 +70,10 @@
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.turtle.FurnaceRefuelHandler;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
@ -79,8 +83,6 @@
import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
import dan200.computercraft.shared.turtle.upgrades.*;
import dan200.computercraft.shared.util.ImpostorRecipe;
import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
@ -359,17 +361,21 @@ private static <T extends CustomRecipe> RegistryEntry<SimpleCraftingRecipeSerial
return REGISTRY.register(name, () -> new SimpleCraftingRecipeSerializer<>(factory));
}
public static final RegistryEntry<RecipeSerializer<CustomShapedRecipe>> SHAPED = REGISTRY.register("shaped", () -> CustomShapedRecipe.serialiser(CustomShapedRecipe::new));
public static final RegistryEntry<RecipeSerializer<CustomShapelessRecipe>> SHAPELESS = REGISTRY.register("shapeless", () -> CustomShapelessRecipe.serialiser(CustomShapelessRecipe::new));
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<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<TurtleRecipe.Serializer> TURTLE = REGISTRY.register("turtle", TurtleRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<TurtleRecipe>> TURTLE = REGISTRY.register("turtle", () -> TurtleRecipe.validatingSerialiser(TurtleRecipe::of));
public static final RegistryEntry<SimpleCraftingRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
public static final RegistryEntry<TurtleOverlayRecipe.Serializer> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
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<ComputerUpgradeRecipe.Serializer> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
public static final RegistryEntry<ImpostorRecipe.Serializer> IMPOSTOR_SHAPED = REGISTRY.register("impostor_shaped", ImpostorRecipe.Serializer::new);
public static final RegistryEntry<ImpostorShapelessRecipe.Serializer> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
}
public static class Permissions {

View File

@ -4,8 +4,33 @@
package dan200.computercraft.shared.computer.core;
public enum ComputerFamily {
NORMAL,
ADVANCED,
COMMAND
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.StringRepresentable;
public enum ComputerFamily implements StringRepresentable {
NORMAL("normal"),
ADVANCED("advanced"),
COMMAND("command");
private final String name;
ComputerFamily(String name) {
this.name = name;
}
public static ComputerFamily getFamily(JsonObject json, String name) {
var familyName = GsonHelper.getAsString(json, name);
for (var family : values()) {
if (family.getSerializedName().equalsIgnoreCase(familyName)) return family;
}
throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
}
@Override
public String getSerializedName() {
return name;
}
}

View File

@ -5,31 +5,20 @@
package dan200.computercraft.shared.computer.recipe;
import dan200.computercraft.shared.computer.items.IComputerItem;
import net.minecraft.core.NonNullList;
import dan200.computercraft.shared.recipe.CustomShapedRecipe;
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.ShapedRecipe;
import net.minecraft.world.level.Level;
/**
* Represents a recipe which converts a computer from one form into another.
* A recipe which converts a computer from one form into another.
*/
public abstract class ComputerConvertRecipe extends ShapedRecipe {
private final String group;
private final ItemStack result;
public ComputerConvertRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
super(identifier, group, category, width, height, ingredients, result);
this.group = group;
this.result = result;
}
public ItemStack getResultItem() {
return result;
public abstract class ComputerConvertRecipe extends CustomShapedRecipe {
public ComputerConvertRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
super(identifier, recipe);
}
protected abstract ItemStack convert(IComputerItem item, ItemStack stack);
@ -55,9 +44,4 @@ public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAc
return ItemStack.EMPTY;
}
@Override
public String getGroup() {
return group;
}
}

View File

@ -1,72 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.computer.recipe;
import com.google.gson.JsonObject;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.util.RecipeUtil;
import net.minecraft.core.NonNullList;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.RecipeSerializer;
public abstract class ComputerFamilyRecipe extends ComputerConvertRecipe {
private final ComputerFamily family;
public ComputerFamilyRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
super(identifier, group, category, width, height, ingredients, result);
this.family = family;
}
public ComputerFamily getFamily() {
return family;
}
public abstract static class Serializer<T extends ComputerFamilyRecipe> implements RecipeSerializer<T> {
protected abstract T create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family);
@Override
public T fromJson(ResourceLocation identifier, JsonObject json) {
var group = GsonHelper.getAsString(json, "group", "");
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
var family = RecipeUtil.getFamily(json, "family");
var template = RecipeUtil.getTemplate(json);
var result = itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
return create(identifier, group, category, template.width(), template.height(), template.ingredients(), result, family);
}
@Override
public T fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
var width = buf.readVarInt();
var height = buf.readVarInt();
var group = buf.readUtf();
var category = buf.readEnum(CraftingBookCategory.class);
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
for (var i = 0; i < ingredients.size(); i++) ingredients.set(i, Ingredient.fromNetwork(buf));
var result = buf.readItem();
var family = buf.readEnum(ComputerFamily.class);
return create(identifier, group, category, width, height, ingredients, result, family);
}
@Override
public void toNetwork(FriendlyByteBuf buf, T recipe) {
buf.writeVarInt(recipe.getWidth());
buf.writeVarInt(recipe.getHeight());
buf.writeUtf(recipe.getGroup());
buf.writeEnum(recipe.category());
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buf);
buf.writeItem(recipe.getResultItem());
buf.writeEnum(recipe.getFamily());
}
}
}

View File

@ -4,35 +4,57 @@
package dan200.computercraft.shared.computer.recipe;
import com.google.gson.JsonObject;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.IComputerItem;
import net.minecraft.core.NonNullList;
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.RecipeSerializer;
public final class ComputerUpgradeRecipe extends ComputerFamilyRecipe {
private ComputerUpgradeRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
super(identifier, group, category, width, height, ingredients, result, family);
/**
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
* family} to another.
*/
public final class ComputerUpgradeRecipe extends ComputerConvertRecipe {
private final ComputerFamily family;
private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe, ComputerFamily family) {
super(identifier, recipe);
this.family = family;
}
@Override
protected ItemStack convert(IComputerItem item, ItemStack stack) {
return item.withFamily(stack, getFamily());
return item.withFamily(stack, family);
}
@Override
public RecipeSerializer<?> getSerializer() {
public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
}
public static class Serializer extends ComputerFamilyRecipe.Serializer<ComputerUpgradeRecipe> {
public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
@Override
protected ComputerUpgradeRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
return new ComputerUpgradeRecipe(identifier, group, category, width, height, ingredients, result, family);
public ComputerUpgradeRecipe fromJson(ResourceLocation identifier, JsonObject json) {
var recipe = ShapedRecipeSpec.fromJson(json);
var family = ComputerFamily.getFamily(json, "family");
return new ComputerUpgradeRecipe(identifier, recipe, family);
}
@Override
public ComputerUpgradeRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
var recipe = ShapedRecipeSpec.fromNetwork(buf);
var family = buf.readEnum(ComputerFamily.class);
return new ComputerUpgradeRecipe(identifier, recipe, family);
}
@Override
public void toNetwork(FriendlyByteBuf buf, ComputerUpgradeRecipe recipe) {
recipe.toSpec().toNetwork(buf);
buf.writeEnum(recipe.family);
}
}
}

View File

@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.mojang.serialization.DataResult;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.Util;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapedRecipe;
/**
* A custom version of {@link ShapedRecipe}, which can be converted to and from a {@link ShapedRecipeSpec}.
* <p>
* This recipe may both be used as a normal recipe (behaving mostly the same as {@link ShapedRecipe}, with
* {@linkplain RecipeUtil#itemStackFromJson(JsonObject) support for putting nbt on the result}), or subclassed to
* customise the crafting behaviour.
*/
public class CustomShapedRecipe extends ShapedRecipe {
private final ItemStack result;
public CustomShapedRecipe(ResourceLocation id, ShapedRecipeSpec recipe) {
super(
id,
recipe.properties().group(), recipe.properties().category(),
recipe.template().width(), recipe.template().height(), recipe.template().ingredients(),
recipe.result()
);
this.result = recipe.result();
}
public final ShapedRecipeSpec toSpec() {
return new ShapedRecipeSpec(RecipeProperties.of(this), ShapedTemplate.of(this), result);
}
@Override
public RecipeSerializer<? extends CustomShapedRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.SHAPED.get();
}
public interface Factory<R> {
R create(ResourceLocation id, ShapedRecipeSpec recipe);
}
public static <T extends CustomShapedRecipe> RecipeSerializer<T> serialiser(CustomShapedRecipe.Factory<T> factory) {
return new Serialiser<>((id, r) -> DataResult.success(factory.create(id, r)));
}
public static <T extends CustomShapedRecipe> RecipeSerializer<T> validatingSerialiser(CustomShapedRecipe.Factory<DataResult<T>> factory) {
return new Serialiser<>(factory);
}
private record Serialiser<T extends CustomShapedRecipe>(
Factory<DataResult<T>> factory
) implements RecipeSerializer<T> {
private Serialiser(Factory<DataResult<T>> factory) {
this.factory = (id, r) -> factory.create(id, r).flatMap(x -> {
if (x.getSerializer() != this) {
return DataResult.error(() -> "Expected serialiser to be " + this + ", but was " + x.getSerializer());
}
return DataResult.success(x);
});
}
@Override
public T fromJson(ResourceLocation id, JsonObject json) {
return Util.getOrThrow(factory.create(id, ShapedRecipeSpec.fromJson(json)), JsonParseException::new);
}
@Override
public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
return Util.getOrThrow(factory.create(id, ShapedRecipeSpec.fromNetwork(buffer)), IllegalStateException::new);
}
@Override
public void toNetwork(FriendlyByteBuf buffer, T recipe) {
recipe.toSpec().toNetwork(buffer);
}
}
}

View File

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.mojang.serialization.DataResult;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.Util;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapelessRecipe;
/**
* A custom version of {@link ShapelessRecipe}, which can be converted to and from a {@link ShapelessRecipeSpec}.
* <p>
* This recipe may both be used as a normal recipe (behaving mostly the same as {@link ShapelessRecipe}, with
* {@linkplain RecipeUtil#itemStackFromJson(JsonObject) support for putting nbt on the result}), or subclassed to
* customise the crafting behaviour.
*/
public class CustomShapelessRecipe extends ShapelessRecipe {
private final ItemStack result;
public CustomShapelessRecipe(ResourceLocation id, ShapelessRecipeSpec recipe) {
super(id, recipe.properties().group(), recipe.properties().category(), recipe.result(), recipe.ingredients());
this.result = recipe.result();
}
public final ShapelessRecipeSpec toSpec() {
return new ShapelessRecipeSpec(RecipeProperties.of(this), getIngredients(), result);
}
@Override
public RecipeSerializer<? extends CustomShapelessRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.SHAPELESS.get();
}
public interface Factory<R> {
R create(ResourceLocation id, ShapelessRecipeSpec recipe);
}
public static <T extends CustomShapelessRecipe> RecipeSerializer<T> serialiser(Factory<T> factory) {
return new CustomShapelessRecipe.Serialiser<>((id, r) -> DataResult.success(factory.create(id, r)));
}
public static <T extends CustomShapelessRecipe> RecipeSerializer<T> validatingSerialiser(Factory<DataResult<T>> factory) {
return new CustomShapelessRecipe.Serialiser<>(factory);
}
private record Serialiser<T extends CustomShapelessRecipe>(
Factory<DataResult<T>> factory
) implements RecipeSerializer<T> {
private Serialiser(Factory<DataResult<T>> factory) {
this.factory = (id, r) -> factory.create(id, r).flatMap(x -> {
if (x.getSerializer() != this) {
return DataResult.error(() -> "Expected serialiser to be " + this + ", but was " + x.getSerializer());
}
return DataResult.success(x);
});
}
@Override
public T fromJson(ResourceLocation id, JsonObject json) {
return Util.getOrThrow(factory.create(id, ShapelessRecipeSpec.fromJson(json)), JsonParseException::new);
}
@Override
public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
return Util.getOrThrow(factory.create(id, ShapelessRecipeSpec.fromNetwork(buffer)), IllegalStateException::new);
}
@Override
public void toNetwork(FriendlyByteBuf buffer, T recipe) {
recipe.toSpec().toNetwork(buffer);
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.recipe;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapedRecipe;
import net.minecraft.world.level.Level;
/**
* A fake {@link ShapedRecipe}, which appears in the recipe book (and other recipe mods), but cannot be crafted.
* <p>
* This is used to represent examples for our {@link CustomRecipe}s.
*/
public final class ImpostorShapedRecipe extends CustomShapedRecipe {
public ImpostorShapedRecipe(ResourceLocation id, ShapedRecipeSpec recipe) {
super(id, recipe);
}
@Override
public boolean matches(CraftingContainer inv, Level world) {
return false;
}
@Override
public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAccess) {
return ItemStack.EMPTY;
}
@Override
public RecipeSerializer<ImpostorShapedRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get();
}
}

View File

@ -0,0 +1,41 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.recipe;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CustomRecipe;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapelessRecipe;
import net.minecraft.world.level.Level;
/**
* A fake {@link ShapelessRecipe}, which appears in the recipe book (and other recipe mods), but cannot be crafted.
* <p>
* This is used to represent examples for our {@link CustomRecipe}s.
*/
public final class ImpostorShapelessRecipe extends CustomShapelessRecipe {
public ImpostorShapelessRecipe(ResourceLocation id, ShapelessRecipeSpec recipe) {
super(id, recipe);
}
@Override
public boolean matches(CraftingContainer inv, Level world) {
return false;
}
@Override
public ItemStack assemble(CraftingContainer inventory, RegistryAccess access) {
return ItemStack.EMPTY;
}
@Override
public RecipeSerializer<ImpostorShapelessRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get();
}
}

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.datafixers.util.Either;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.TagParser;
/**
* Additional codecs for working with recipes.
*/
public class MoreCodecs {
/**
* A codec for {@link CompoundTag}s, which either accepts a NBT-string or a JSON object.
*/
public static final Codec<CompoundTag> TAG = Codec.either(Codec.STRING, CompoundTag.CODEC).flatXmap(
either -> either.map(MoreCodecs::parseTag, DataResult::success),
nbtCompound -> DataResult.success(Either.left(nbtCompound.getAsString()))
);
private static DataResult<CompoundTag> parseTag(String contents) {
try {
return DataResult.success(TagParser.parseTag(contents));
} catch (CommandSyntaxException e) {
return DataResult.error(e::getMessage);
}
}
}

View File

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.CraftingRecipe;
/**
* Common properties that appear in all {@link CraftingRecipe}s.
*
* @param group The (optional) group of the recipe, see {@link CraftingRecipe#getGroup()}.
* @param category The category the recipe appears in, see {@link CraftingRecipe#category()}.
*/
public record RecipeProperties(String group, CraftingBookCategory category) {
public static RecipeProperties of(CraftingRecipe recipe) {
return new RecipeProperties(recipe.getGroup(), recipe.category());
}
public static RecipeProperties fromJson(JsonObject json) {
var group = GsonHelper.getAsString(json, "group", "");
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
return new RecipeProperties(group, category);
}
public static RecipeProperties fromNetwork(FriendlyByteBuf buffer) {
var group = buffer.readUtf();
var category = buffer.readEnum(CraftingBookCategory.class);
return new RecipeProperties(group, category);
}
public void toNetwork(FriendlyByteBuf buffer) {
buffer.writeUtf(group());
buffer.writeEnum(category());
}
}

View File

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.mojang.serialization.JsonOps;
import net.minecraft.Util;
import net.minecraft.core.NonNullList;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.ShapedRecipe;
public final class RecipeUtil {
private RecipeUtil() {
}
public static NonNullList<Ingredient> readIngredients(FriendlyByteBuf buffer) {
return buffer.readCollection(x -> NonNullList.withSize(x, Ingredient.EMPTY), Ingredient::fromNetwork);
}
public static void writeIngredients(FriendlyByteBuf buffer, NonNullList<Ingredient> ingredients) {
buffer.writeCollection(ingredients, (a, b) -> b.toNetwork(a));
}
public static NonNullList<Ingredient> readShapelessIngredients(JsonObject json) {
NonNullList<Ingredient> ingredients = NonNullList.create();
var ingredientsList = GsonHelper.getAsJsonArray(json, "ingredients");
for (var i = 0; i < ingredientsList.size(); ++i) {
var ingredient = Ingredient.fromJson(ingredientsList.get(i));
if (!ingredient.isEmpty()) ingredients.add(ingredient);
}
if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
if (ingredients.size() > 9) {
throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
}
return ingredients;
}
/**
* Extends {@link ShapedRecipe#itemStackFromJson(JsonObject)} with support for the {@code nbt} field.
*
* @param json The json to extract the item from.
* @return The parsed item stack.
*/
public static ItemStack itemStackFromJson(JsonObject json) {
var item = ShapedRecipe.itemFromJson(json);
if (json.has("data")) throw new JsonParseException("Disallowed data tag found");
var count = GsonHelper.getAsInt(json, "count", 1);
if (count < 1) throw new JsonSyntaxException("Invalid output count: " + count);
var stack = new ItemStack(item, count);
var nbt = json.get("nbt");
if (nbt != null) {
stack.setTag(Util.getOrThrow(MoreCodecs.TAG.parse(JsonOps.INSTANCE, nbt), JsonParseException::new));
}
return stack;
}
}

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.ShapedRecipe;
/**
* A description of a {@link ShapedRecipe}.
* <p>
* This is meant to be used in conjunction with {@link CustomShapedRecipe} for more reusable serialisation and
* deserialisation of {@link ShapedRecipe}-like recipes.
*
* @param properties The common properties of this recipe.
* @param template The shaped template of the recipe.
* @param result The result of the recipe.
*/
public record ShapedRecipeSpec(RecipeProperties properties, ShapedTemplate template, ItemStack result) {
public static ShapedRecipeSpec fromJson(JsonObject json) {
var properties = RecipeProperties.fromJson(json);
var template = ShapedTemplate.fromJson(json);
var result = RecipeUtil.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
return new ShapedRecipeSpec(properties, template, result);
}
public static ShapedRecipeSpec fromNetwork(FriendlyByteBuf buffer) {
var properties = RecipeProperties.fromNetwork(buffer);
var template = ShapedTemplate.fromNetwork(buffer);
var result = buffer.readItem();
return new ShapedRecipeSpec(properties, template, result);
}
public void toNetwork(FriendlyByteBuf buffer) {
properties().toNetwork(buffer);
template().toNetwork(buffer);
buffer.writeItem(result());
}
}

View File

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import net.minecraft.core.NonNullList;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.ShapedRecipe;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* The template for {@linkplain ShapedRecipe shaped recipes}. This largely exists for parsing shaped recipes from JSON.
*
* @param width The width of the recipe, see {@link ShapedRecipe#getWidth()}.
* @param height The height of the recipe, see {@link ShapedRecipe#getHeight()}.
* @param ingredients The ingredients in the recipe, see {@link ShapedRecipe#getIngredients()}
*/
public record ShapedTemplate(int width, int height, NonNullList<Ingredient> ingredients) {
public static ShapedTemplate of(ShapedRecipe recipe) {
return new ShapedTemplate(recipe.getWidth(), recipe.getHeight(), recipe.getIngredients());
}
public static ShapedTemplate fromJson(JsonObject json) {
Map<Character, Ingredient> key = new HashMap<>();
for (var entry : GsonHelper.getAsJsonObject(json, "key").entrySet()) {
if (entry.getKey().length() != 1) {
throw new JsonSyntaxException("Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only).");
}
if (" ".equals(entry.getKey())) {
throw new JsonSyntaxException("Invalid key entry: ' ' is a reserved symbol.");
}
key.put(entry.getKey().charAt(0), Ingredient.fromJson(entry.getValue()));
}
var patternList = GsonHelper.getAsJsonArray(json, "pattern");
if (patternList.size() == 0) {
throw new JsonSyntaxException("Invalid pattern: empty pattern not allowed");
}
var pattern = new String[patternList.size()];
for (var x = 0; x < pattern.length; x++) {
var line = GsonHelper.convertToString(patternList.get(x), "pattern[" + x + "]");
if (x > 0 && pattern[0].length() != line.length()) {
throw new JsonSyntaxException("Invalid pattern: each row must be the same width");
}
pattern[x] = line;
}
var width = pattern[0].length();
var height = pattern.length;
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
Set<Character> missingKeys = new HashSet<>(key.keySet());
var ingredientIdx = 0;
for (var line : pattern) {
for (var x = 0; x < line.length(); x++) {
var chr = line.charAt(x);
var ing = chr == ' ' ? Ingredient.EMPTY : key.get(chr);
if (ing == null) {
throw new JsonSyntaxException("Pattern references symbol '" + chr + "' but it's not defined in the key");
}
ingredients.set(ingredientIdx++, ing);
missingKeys.remove(chr);
}
}
if (!missingKeys.isEmpty()) {
throw new JsonSyntaxException("Key defines symbols that aren't used in pattern: " + missingKeys);
}
return new ShapedTemplate(width, height, ingredients);
}
public static ShapedTemplate fromNetwork(FriendlyByteBuf buffer) {
var width = buffer.readVarInt();
var height = buffer.readVarInt();
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
for (var i = 0; i < ingredients.size(); ++i) ingredients.set(i, Ingredient.fromNetwork(buffer));
return new ShapedTemplate(width, height, ingredients);
}
public void toNetwork(FriendlyByteBuf buffer) {
buffer.writeVarInt(width());
buffer.writeVarInt(height());
for (var ingredient : ingredients) ingredient.toNetwork(buffer);
}
}

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.recipe;
import com.google.gson.JsonObject;
import net.minecraft.core.NonNullList;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.ShapelessRecipe;
/**
* A description of a {@link ShapelessRecipe}.
* <p>
* This is meant to be used in conjunction with {@link CustomShapelessRecipe} for more reusable serialisation and
* deserialisation of {@link ShapelessRecipe}-like recipes.
*
* @param properties The common properties of this recipe.
* @param ingredients The ingredients of the recipe.
* @param result The result of the recipe.
*/
public record ShapelessRecipeSpec(RecipeProperties properties, NonNullList<Ingredient> ingredients, ItemStack result) {
public static ShapelessRecipeSpec fromJson(JsonObject json) {
var properties = RecipeProperties.fromJson(json);
var ingredients = RecipeUtil.readShapelessIngredients(json);
var result = RecipeUtil.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
return new ShapelessRecipeSpec(properties, ingredients, result);
}
public static ShapelessRecipeSpec fromNetwork(FriendlyByteBuf buffer) {
var properties = RecipeProperties.fromNetwork(buffer);
var ingredients = RecipeUtil.readIngredients(buffer);
var result = buffer.readItem();
return new ShapelessRecipeSpec(properties, ingredients, result);
}
public void toNetwork(FriendlyByteBuf buffer) {
properties().toNetwork(buffer);
RecipeUtil.writeIngredients(buffer, ingredients());
buffer.writeItem(result());
}
}

View File

@ -4,32 +4,30 @@
package dan200.computercraft.shared.turtle.recipes;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
import dan200.computercraft.shared.recipe.ShapelessRecipeSpec;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.core.NonNullList;
import net.minecraft.core.RegistryAccess;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.*;
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 ShapelessRecipe {
public class TurtleOverlayRecipe extends CustomShapelessRecipe {
private final ResourceLocation overlay;
private final ItemStack result;
public TurtleOverlayRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients, ResourceLocation overlay) {
super(id, group, category, result, ingredients);
public TurtleOverlayRecipe(ResourceLocation id, ShapelessRecipeSpec spec, ResourceLocation overlay) {
super(id, spec);
this.overlay = overlay;
this.result = result;
}
private static ItemStack make(ItemStack stack, ResourceLocation overlay) {
@ -56,63 +54,29 @@ public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAc
}
@Override
public RecipeSerializer<?> getSerializer() {
public RecipeSerializer<TurtleOverlayRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
}
public static class Serializer implements RecipeSerializer<TurtleOverlayRecipe> {
@Override
public TurtleOverlayRecipe fromJson(ResourceLocation id, JsonObject json) {
var group = GsonHelper.getAsString(json, "group", "");
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
var ingredients = readIngredients(GsonHelper.getAsJsonArray(json, "ingredients"));
if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
if (ingredients.size() > 9) {
throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
}
var recipe = ShapelessRecipeSpec.fromJson(json);
var overlay = new ResourceLocation(GsonHelper.getAsString(json, "overlay"));
// We could derive this from the ingredients, but we want to avoid evaluating the ingredients too early, so
// it's easier to do this.
var result = make(ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")), overlay);
return new TurtleOverlayRecipe(id, group, category, result, ingredients, overlay);
}
private NonNullList<Ingredient> readIngredients(JsonArray arrays) {
NonNullList<Ingredient> items = NonNullList.create();
for (var i = 0; i < arrays.size(); ++i) {
var ingredient = Ingredient.fromJson(arrays.get(i));
if (!ingredient.isEmpty()) items.add(ingredient);
}
return items;
return new TurtleOverlayRecipe(id, recipe, overlay);
}
@Override
public TurtleOverlayRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
var group = buffer.readUtf();
var category = buffer.readEnum(CraftingBookCategory.class);
var count = buffer.readVarInt();
var items = NonNullList.withSize(count, Ingredient.EMPTY);
for (var j = 0; j < items.size(); j++) items.set(j, Ingredient.fromNetwork(buffer));
var result = buffer.readItem();
var recipe = ShapelessRecipeSpec.fromNetwork(buffer);
var overlay = buffer.readResourceLocation();
return new TurtleOverlayRecipe(id, group, category, result, items, overlay);
return new TurtleOverlayRecipe(id, recipe, overlay);
}
@Override
public void toNetwork(FriendlyByteBuf buffer, TurtleOverlayRecipe recipe) {
buffer.writeUtf(recipe.getGroup());
buffer.writeEnum(recipe.category());
buffer.writeVarInt(recipe.getIngredients().size());
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
buffer.writeItem(recipe.result);
recipe.toSpec().toNetwork(buffer);
buffer.writeResourceLocation(recipe.overlay);
}
}

View File

@ -4,26 +4,33 @@
package dan200.computercraft.shared.turtle.recipes;
import com.mojang.serialization.DataResult;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.computer.recipe.ComputerFamilyRecipe;
import dan200.computercraft.shared.computer.recipe.ComputerConvertRecipe;
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.core.NonNullList;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.RecipeSerializer;
public final class TurtleRecipe extends ComputerFamilyRecipe {
public TurtleRecipe(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
super(identifier, group, category, width, height, ingredients, result, family);
/**
* The recipe which crafts a turtle from an existing computer item.
*/
public final class TurtleRecipe extends ComputerConvertRecipe {
private final TurtleItem turtle;
private TurtleRecipe(ResourceLocation id, ShapedRecipeSpec recipe, TurtleItem turtle) {
super(id, recipe);
this.turtle = turtle;
}
@Override
public RecipeSerializer<?> getSerializer() {
return ModRegistry.RecipeSerializers.TURTLE.get();
public static DataResult<TurtleRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
if (!(recipe.result().getItem() instanceof TurtleItem turtle)) {
return DataResult.error(() -> recipe.result().getItem() + " is not a turtle item");
}
return DataResult.success(new TurtleRecipe(id, recipe, turtle));
}
@Override
@ -31,13 +38,11 @@ protected ItemStack convert(IComputerItem item, ItemStack stack) {
var computerID = item.getComputerID(stack);
var label = item.getLabel(stack);
return TurtleItem.create(computerID, label, -1, getFamily(), null, null, 0, null);
return turtle.create(computerID, label, -1, null, null, 0, null);
}
public static class Serializer extends ComputerFamilyRecipe.Serializer<TurtleRecipe> {
@Override
protected TurtleRecipe create(ResourceLocation identifier, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family) {
return new TurtleRecipe(identifier, group, category, width, height, ingredients, result, family);
}
@Override
public RecipeSerializer<TurtleRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.TURTLE.get();
}
}

View File

@ -1,88 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.util;
import com.google.gson.JsonObject;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.NonNullList;
import net.minecraft.core.RegistryAccess;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.ShapedRecipe;
import net.minecraft.world.level.Level;
public final class ImpostorRecipe extends ShapedRecipe {
private final String group;
private final ItemStack result;
private ImpostorRecipe(ResourceLocation id, String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
super(id, group, category, width, height, ingredients, result);
this.group = group;
this.result = result;
}
@Override
public String getGroup() {
return group;
}
ItemStack getResultItem() {
return result;
}
@Override
public boolean matches(CraftingContainer inv, Level world) {
return false;
}
@Override
public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryAccess) {
return ItemStack.EMPTY;
}
@Override
public RecipeSerializer<?> getSerializer() {
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get();
}
public static class Serializer implements RecipeSerializer<ImpostorRecipe> {
@Override
public ImpostorRecipe fromJson(ResourceLocation identifier, JsonObject json) {
var group = GsonHelper.getAsString(json, "group", "");
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
var recipe = RecipeSerializer.SHAPED_RECIPE.fromJson(identifier, json);
var result = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
return new ImpostorRecipe(identifier, group, category, recipe.getWidth(), recipe.getHeight(), recipe.getIngredients(), result);
}
@Override
public ImpostorRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
var width = buf.readVarInt();
var height = buf.readVarInt();
var group = buf.readUtf(Short.MAX_VALUE);
var category = buf.readEnum(CraftingBookCategory.class);
var items = NonNullList.withSize(width * height, Ingredient.EMPTY);
for (var k = 0; k < items.size(); k++) items.set(k, Ingredient.fromNetwork(buf));
var result = buf.readItem();
return new ImpostorRecipe(identifier, group, category, width, height, items, result);
}
@Override
public void toNetwork(FriendlyByteBuf buf, ImpostorRecipe recipe) {
buf.writeVarInt(recipe.getWidth());
buf.writeVarInt(recipe.getHeight());
buf.writeUtf(recipe.getGroup());
buf.writeEnum(recipe.category());
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buf);
buf.writeItem(recipe.getResultItem());
}
}
}

View File

@ -1,104 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.util;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.NonNullList;
import net.minecraft.core.RegistryAccess;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.inventory.CraftingContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.*;
import net.minecraft.world.level.Level;
public final class ImpostorShapelessRecipe extends ShapelessRecipe {
private final String group;
private final ItemStack result;
private ImpostorShapelessRecipe(ResourceLocation id, String group, CraftingBookCategory category, ItemStack result, NonNullList<Ingredient> ingredients) {
super(id, group, category, result, ingredients);
this.group = group;
this.result = result;
}
@Override
public String getGroup() {
return group;
}
ItemStack getResultItem() {
return result;
}
@Override
public boolean matches(CraftingContainer inv, Level world) {
return false;
}
@Override
public ItemStack assemble(CraftingContainer inventory, RegistryAccess access) {
return ItemStack.EMPTY;
}
@Override
public RecipeSerializer<?> getSerializer() {
return ModRegistry.RecipeSerializers.IMPOSTOR_SHAPELESS.get();
}
public static final class Serializer implements RecipeSerializer<ImpostorShapelessRecipe> {
@Override
public ImpostorShapelessRecipe fromJson(ResourceLocation id, JsonObject json) {
var group = GsonHelper.getAsString(json, "group", "");
var category = CraftingBookCategory.CODEC.byName(GsonHelper.getAsString(json, "category", null), CraftingBookCategory.MISC);
var ingredients = readIngredients(GsonHelper.getAsJsonArray(json, "ingredients"));
if (ingredients.isEmpty()) throw new JsonParseException("No ingredients for shapeless recipe");
if (ingredients.size() > 9) {
throw new JsonParseException("Too many ingredients for shapeless recipe the max is 9");
}
var result = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"));
return new ImpostorShapelessRecipe(id, group, category, result, ingredients);
}
private NonNullList<Ingredient> readIngredients(JsonArray arrays) {
NonNullList<Ingredient> items = NonNullList.create();
for (var i = 0; i < arrays.size(); ++i) {
var ingredient = Ingredient.fromJson(arrays.get(i));
if (!ingredient.isEmpty()) items.add(ingredient);
}
return items;
}
@Override
public ImpostorShapelessRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) {
var group = buffer.readUtf();
var category = buffer.readEnum(CraftingBookCategory.class);
var count = buffer.readVarInt();
var items = NonNullList.withSize(count, Ingredient.EMPTY);
for (var j = 0; j < items.size(); j++) items.set(j, Ingredient.fromNetwork(buffer));
var result = buffer.readItem();
return new ImpostorShapelessRecipe(id, group, category, result, items);
}
@Override
public void toNetwork(FriendlyByteBuf buffer, ImpostorShapelessRecipe recipe) {
buffer.writeUtf(recipe.getGroup());
buffer.writeEnum(recipe.category());
buffer.writeVarInt(recipe.getIngredients().size());
for (var ingredient : recipe.getIngredients()) ingredient.toNetwork(buffer);
buffer.writeItem(recipe.getResultItem());
}
}
}

View File

@ -1,94 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.core.NonNullList;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.crafting.Ingredient;
import java.util.Map;
import java.util.Set;
// TODO: Replace some things with Forge??
public final class RecipeUtil {
private RecipeUtil() {
}
public record ShapedTemplate(int width, int height, NonNullList<Ingredient> ingredients) {
}
public static ShapedTemplate getTemplate(JsonObject json) {
Map<Character, Ingredient> ingMap = Maps.newHashMap();
for (var entry : GsonHelper.getAsJsonObject(json, "key").entrySet()) {
if (entry.getKey().length() != 1) {
throw new JsonSyntaxException("Invalid key entry: '" + entry.getKey() + "' is an invalid symbol (must be 1 character only).");
}
if (" ".equals(entry.getKey())) {
throw new JsonSyntaxException("Invalid key entry: ' ' is a reserved symbol.");
}
ingMap.put(entry.getKey().charAt(0), Ingredient.fromJson(entry.getValue()));
}
ingMap.put(' ', Ingredient.EMPTY);
var patternJ = GsonHelper.getAsJsonArray(json, "pattern");
if (patternJ.size() == 0) {
throw new JsonSyntaxException("Invalid pattern: empty pattern not allowed");
}
var pattern = new String[patternJ.size()];
for (var x = 0; x < pattern.length; x++) {
var line = GsonHelper.convertToString(patternJ.get(x), "pattern[" + x + "]");
if (x > 0 && pattern[0].length() != line.length()) {
throw new JsonSyntaxException("Invalid pattern: each row must be the same width");
}
pattern[x] = line;
}
var width = pattern[0].length();
var height = pattern.length;
var ingredients = NonNullList.withSize(width * height, Ingredient.EMPTY);
Set<Character> missingKeys = Sets.newHashSet(ingMap.keySet());
missingKeys.remove(' ');
var ingredientIdx = 0;
for (var line : pattern) {
for (var i = 0; i < line.length(); i++) {
var chr = line.charAt(i);
var ing = ingMap.get(chr);
if (ing == null) {
throw new JsonSyntaxException("Pattern references symbol '" + chr + "' but it's not defined in the key");
}
ingredients.set(ingredientIdx++, ing);
missingKeys.remove(chr);
}
}
if (!missingKeys.isEmpty()) {
throw new JsonSyntaxException("Key defines symbols that aren't used in pattern: " + missingKeys);
}
return new ShapedTemplate(width, height, ingredients);
}
public static ComputerFamily getFamily(JsonObject json, String name) {
var familyName = GsonHelper.getAsString(json, name);
for (var family : ComputerFamily.values()) {
if (family.name().equalsIgnoreCase(familyName)) return family;
}
throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
}
}

View File

@ -4,6 +4,7 @@
package dan200.computercraft.gametest
import com.mojang.authlib.GameProfile
import dan200.computercraft.gametest.api.Structures
import dan200.computercraft.gametest.api.sequence
import dan200.computercraft.shared.ModRegistry
@ -11,6 +12,7 @@
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtUtils
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.MenuType
@ -41,11 +43,10 @@ fun Craft_result_has_nbt(context: GameTestHelper) = context.sequence {
val result = recipe.get().assemble(container, context.level.registryAccess())
val owner = CompoundTag()
owner.putString("Name", "dan200")
owner.putString("Id", "f3c8d69b-0776-4512-8434-d1b2165909eb")
val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")
val tag = CompoundTag()
tag.put("SkullOwner", owner)
tag.put("SkullOwner", NbtUtils.writeGameProfile(CompoundTag(), profile))
assertEquals(tag, result.tag, "Expected NBT tags to be the same")
}

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:computer_upgrade",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {"#": {"tag": "c:gold_ingots"}, "C": {"item": "computercraft:computer_normal"}},
"pattern": ["###", "#C#", "# #"],
"result": {"item": "computercraft:computer_advanced"},

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:computer_upgrade",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {"#": {"tag": "c:gold_ingots"}, "C": {"item": "computercraft:pocket_computer_normal"}},
"pattern": ["###", "#C#", "# #"],
"result": {"item": "computercraft:pocket_computer_advanced"},

View File

@ -1,9 +1,9 @@
{
"type": "minecraft:crafting_shapeless",
"type": "computercraft:shapeless",
"category": "misc",
"ingredients": [{"tag": "c:skulls"}, {"item": "computercraft:monitor_normal"}],
"result": {
"item": "minecraft:player_head",
"nbt": "{SkullOwner:{Id:\"6d074736-b1e9-4378-a99b-bd8777821c9c\",Name:\"Cloudhunter\"}}"
"nbt": "{SkullOwner:{Id:[I;1829193526,-1310112904,-1449411193,2005015708],Name:\"Cloudhunter\"}}"
}
}

View File

@ -1,9 +1,9 @@
{
"type": "minecraft:crafting_shapeless",
"type": "computercraft:shapeless",
"category": "misc",
"ingredients": [{"tag": "c:skulls"}, {"item": "computercraft:computer_advanced"}],
"result": {
"item": "minecraft:player_head",
"nbt": "{SkullOwner:{Id:\"f3c8d69b-0776-4512-8434-d1b2165909eb\",Name:\"dan200\"}}"
"nbt": "{SkullOwner:{Id:[I;-204941669,125191442,-2076913230,374933995],Name:\"dan200\"}}"
}
}

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:turtle",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {
"#": {"tag": "c:gold_ingots"},
"C": {"item": "computercraft:computer_advanced"},

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:computer_upgrade",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {
"#": {"tag": "c:gold_ingots"},
"B": {"item": "minecraft:gold_block"},

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:turtle",
"category": "redstone",
"family": "NORMAL",
"family": "normal",
"key": {
"#": {"tag": "c:iron_ingots"},
"C": {"item": "computercraft:computer_normal"},

View File

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin;
import com.google.gson.JsonObject;
import dan200.computercraft.shared.FabricCommonHooks;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.ShapedRecipe;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ShapedRecipe.class)
class ShapedRecipeMixin {
@Inject(method = "itemStackFromJson", at = @At("RETURN"))
@SuppressWarnings("UnusedMethod")
private static void itemStackFromJson(JsonObject json, CallbackInfoReturnable<ItemStack> cir) {
// This is a fairly invasive mixin in the sense that every mod goes through this code path. We might want to
// remove this and use custom recipes types in the future.
FabricCommonHooks.addRecipeResultTag(cir.getReturnValue(), json);
}
}

View File

@ -4,18 +4,12 @@
package dan200.computercraft.shared;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.TagParser;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
@ -24,15 +18,10 @@
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
public class FabricCommonHooks {
private static final Gson GSON = new GsonBuilder().create();
private static final Logger LOGGER = LoggerFactory.getLogger(FabricCommonHooks.class);
public static boolean onBlockDestroy(Level level, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity) {
return !(state.getBlock() instanceof CableBlock cable) || !cable.onCustomDestroyBlock(state, level, pos, player);
}
@ -64,23 +53,4 @@ public static InteractionResult useOnBlock(Player player, Level level, Interacti
private static boolean doesSneakBypassUse(ItemStack stack) {
return stack.isEmpty() || stack.getItem() instanceof DiskItem || stack.getItem() instanceof TreasureDiskItem;
}
/**
* Add the {@code "nbt"} field to the resulting item stack.
*
* @param stack The stack to add the tag to.
* @param json The result JSON object to parse.
*/
public static void addRecipeResultTag(ItemStack stack, JsonObject json) {
var nbt = json.get("nbt");
if (nbt == null || stack.hasTag()) return;
try {
stack.setTag(nbt.isJsonObject()
? TagParser.parseTag(GSON.toJson(nbt))
: TagParser.parseTag(GsonHelper.convertToString(nbt, "nbt")));
} catch (CommandSyntaxException e) {
LOGGER.error("Invalid NBT entry {}, skipping.", nbt);
}
}
}

View File

@ -14,7 +14,6 @@
"ItemEntityMixin",
"ItemMixin",
"ServerLevelMixin",
"ShapedRecipeMixin",
"TagEntryAccessor",
"TagsProviderMixin"
]

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:computer_upgrade",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {"#": {"tag": "forge:ingots/gold"}, "C": {"item": "computercraft:computer_normal"}},
"pattern": ["###", "#C#", "# #"],
"result": {"item": "computercraft:computer_advanced"},

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:computer_upgrade",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {"#": {"tag": "forge:ingots/gold"}, "C": {"item": "computercraft:pocket_computer_normal"}},
"pattern": ["###", "#C#", "# #"],
"result": {"item": "computercraft:pocket_computer_advanced"},

View File

@ -1,9 +1,9 @@
{
"type": "minecraft:crafting_shapeless",
"type": "computercraft:shapeless",
"category": "misc",
"ingredients": [{"tag": "forge:heads"}, {"item": "computercraft:monitor_normal"}],
"result": {
"item": "minecraft:player_head",
"nbt": "{SkullOwner:{Id:\"6d074736-b1e9-4378-a99b-bd8777821c9c\",Name:\"Cloudhunter\"}}"
"nbt": "{SkullOwner:{Id:[I;1829193526,-1310112904,-1449411193,2005015708],Name:\"Cloudhunter\"}}"
}
}

View File

@ -1,9 +1,9 @@
{
"type": "minecraft:crafting_shapeless",
"type": "computercraft:shapeless",
"category": "misc",
"ingredients": [{"tag": "forge:heads"}, {"item": "computercraft:computer_advanced"}],
"result": {
"item": "minecraft:player_head",
"nbt": "{SkullOwner:{Id:\"f3c8d69b-0776-4512-8434-d1b2165909eb\",Name:\"dan200\"}}"
"nbt": "{SkullOwner:{Id:[I;-204941669,125191442,-2076913230,374933995],Name:\"dan200\"}}"
}
}

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:turtle",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {
"#": {"tag": "forge:ingots/gold"},
"C": {"item": "computercraft:computer_advanced"},

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:computer_upgrade",
"category": "redstone",
"family": "ADVANCED",
"family": "advanced",
"key": {
"#": {"tag": "forge:ingots/gold"},
"B": {"tag": "forge:storage_blocks/gold"},

View File

@ -1,7 +1,7 @@
{
"type": "computercraft:turtle",
"category": "redstone",
"family": "NORMAL",
"family": "normal",
"key": {
"#": {"tag": "forge:ingots/iron"},
"C": {"item": "computercraft:computer_normal"},