From ad70e2ad906026e59c900c28fde5a1918cab90f4 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 9 May 2024 18:47:22 +0100 Subject: [PATCH] Make printout recipes a little more flexible Rather than having one single hard-coded recipe, we now have separate recipes for printed pages and printed books. These recipes are defined in terms of - A list of ingredients (like shapeless recipes). - A result item. - An ingredient defining the acceptable page items (so printed page(s), but not books). This cannot overlap with any of the main ingredients. - The minimum number of printouts required. We then override the shapeless recipe crafting logic to allow for multiple printouts to appear. It feels like it'd be nice to generalise this to a way of defining shapeless recipes with variable-count ingredients (for instance, the disk recipe could also be defined this way), but I don't think it's worth it right now. This solves some of the issues in #1755. Disk recipes have not been changed yet. --- .../computercraft/recipes/printed_pages.json | 10 +- .../data/computercraft/recipes/printout.json | 1 - .../computercraft/data/RecipeProvider.java | 17 +- .../data/recipe/ShapelessSpecBuilder.java | 3 +- .../computercraft/shared/ModRegistry.java | 14 +- .../shared/media/recipes/PrintoutRecipe.java | 194 ++++++++++-------- .../shared/recipe/ShapelessRecipeSpec.java | 9 + projects/fabric/build.gradle.kts | 1 + .../computercraft/recipes/printed_book.json | 10 +- .../computercraft/recipes/printed_book.json | 10 +- 10 files changed, 162 insertions(+), 107 deletions(-) delete mode 100644 projects/common/src/generated/resources/data/computercraft/recipes/printout.json diff --git a/projects/common/src/generated/resources/data/computercraft/recipes/printed_pages.json b/projects/common/src/generated/resources/data/computercraft/recipes/printed_pages.json index 7db88a0ee..6b8f2a00b 100644 --- a/projects/common/src/generated/resources/data/computercraft/recipes/printed_pages.json +++ b/projects/common/src/generated/resources/data/computercraft/recipes/printed_pages.json @@ -1,10 +1,12 @@ { - "type": "computercraft:impostor_shapeless", + "type": "computercraft:printout", "category": "redstone", - "ingredients": [ + "ingredients": [{"tag": "c:strings"}], + "min_printouts": 2, + "printout": [ {"item": "computercraft:printed_page"}, - {"item": "computercraft:printed_page"}, - {"tag": "c:strings"} + {"item": "computercraft:printed_pages"}, + {"item": "minecraft:paper"} ], "result": {"count": 1, "id": "computercraft:printed_pages"} } diff --git a/projects/common/src/generated/resources/data/computercraft/recipes/printout.json b/projects/common/src/generated/resources/data/computercraft/recipes/printout.json deleted file mode 100644 index bff250bb7..000000000 --- a/projects/common/src/generated/resources/data/computercraft/recipes/printout.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "computercraft:printout", "category": "misc"} diff --git a/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java b/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java index cc287f471..8d7233077 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java +++ b/projects/common/src/main/java/dan200/computercraft/data/RecipeProvider.java @@ -98,7 +98,6 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider { turtleUpgrades(add, registries); turtleOverlays(add); - addSpecial(add, new PrintoutRecipe(CraftingBookCategory.MISC)); addSpecial(add, new DiskRecipe(CraftingBookCategory.MISC)); addSpecial(add, new ColourableRecipe(CraftingBookCategory.MISC)); addSpecial(add, new ClearColourRecipe(CraftingBookCategory.MISC)); @@ -470,21 +469,25 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider { .build() .save(add, new ResourceLocation(ComputerCraftAPI.MOD_ID, "skull_dan200")); + var pages = Ingredient.of( + ModRegistry.Items.PRINTED_PAGE.get(), + ModRegistry.Items.PRINTED_PAGES.get(), + Items.PAPER + ); + ShapelessSpecBuilder .shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get()) - .requires(ModRegistry.Items.PRINTED_PAGE.get(), 2) .requires(ingredients.string()) - .unlockedBy("has_printer", inventoryChange(ModRegistry.Blocks.PRINTER.get())) - .build(ImpostorShapelessRecipe::new) + .unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get())) + .build(x -> new PrintoutRecipe(x, pages, 2)) .save(add); ShapelessSpecBuilder .shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get()) .requires(ingredients.leather()) - .requires(ModRegistry.Items.PRINTED_PAGE.get(), 1) .requires(ingredients.string()) - .unlockedBy("has_printer", inventoryChange(ModRegistry.Blocks.PRINTER.get())) - .build(ImpostorShapelessRecipe::new) + .unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get())) + .build(x -> new PrintoutRecipe(x, pages, 1)) .save(add); } diff --git a/projects/common/src/main/java/dan200/computercraft/data/recipe/ShapelessSpecBuilder.java b/projects/common/src/main/java/dan200/computercraft/data/recipe/ShapelessSpecBuilder.java index 29f49e221..245eeb01a 100644 --- a/projects/common/src/main/java/dan200/computercraft/data/recipe/ShapelessSpecBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/data/recipe/ShapelessSpecBuilder.java @@ -13,7 +13,6 @@ import net.minecraft.tags.TagKey; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.ShapelessRecipe; import net.minecraft.world.level.ItemLike; /** @@ -61,6 +60,6 @@ public final class ShapelessSpecBuilder extends AbstractRecipeBuilder new ShapelessRecipe(spec.properties().group(), spec.properties().category(), spec.result(), spec.ingredients())); + return build(ShapelessRecipeSpec::create); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index 08b3b69b0..a46489491 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -249,12 +249,16 @@ public final class ModRegistry { public static final RegistryEntry TREASURE_DISK = REGISTRY.register("treasure_disk", () -> new TreasureDiskItem(properties().stacksTo(1))); + private static Item.Properties printoutProperties() { + return properties().stacksTo(1).component(DataComponents.PRINTOUT.get(), PrintoutData.EMPTY); + } + public static final RegistryEntry PRINTED_PAGE = REGISTRY.register("printed_page", - () -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGE)); + () -> new PrintoutItem(printoutProperties(), PrintoutItem.Type.PAGE)); public static final RegistryEntry PRINTED_PAGES = REGISTRY.register("printed_pages", - () -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGES)); + () -> new PrintoutItem(printoutProperties(), PrintoutItem.Type.PAGES)); public static final RegistryEntry PRINTED_BOOK = REGISTRY.register("printed_book", - () -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.BOOK)); + () -> new PrintoutItem(printoutProperties(), PrintoutItem.Type.BOOK)); public static final RegistryEntry SPEAKER = ofBlock(Blocks.SPEAKER, BlockItem::new); public static final RegistryEntry DISK_DRIVE = ofBlock(Blocks.DISK_DRIVE, BlockItem::new); @@ -488,7 +492,7 @@ public final class ModRegistry { public static final RegistryEntry> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new); public static final RegistryEntry> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new); public static final RegistryEntry> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new); - public static final RegistryEntry> PRINTOUT = simple("printout", PrintoutRecipe::new); + public static final RegistryEntry> PRINTOUT = register("printout", PrintoutRecipe.CODEC, PrintoutRecipe.STREAM_CODEC); public static final RegistryEntry> DISK = simple("disk", DiskRecipe::new); } @@ -560,8 +564,8 @@ public final class ModRegistry { public static void register() { Blocks.REGISTRY.register(); BlockEntities.REGISTRY.register(); - Items.REGISTRY.register(); DataComponents.REGISTRY.register(); + Items.REGISTRY.register(); TurtleUpgradeTypes.REGISTRY.register(); PocketUpgradeTypes.REGISTRY.register(); Menus.REGISTRY.register(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java b/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java index 7ba2ef59b..a99635f0f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java @@ -4,115 +4,141 @@ package dan200.computercraft.shared.media.recipes; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.media.items.PrintoutData; import dan200.computercraft.shared.media.items.PrintoutItem; -import dan200.computercraft.shared.platform.PlatformHelper; -import dan200.computercraft.shared.util.DataComponentUtil; +import dan200.computercraft.shared.recipe.RecipeProperties; +import dan200.computercraft.shared.recipe.ShapelessRecipeSpec; import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.inventory.CraftingContainer; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import net.minecraft.world.item.crafting.CraftingBookCategory; -import net.minecraft.world.item.crafting.CustomRecipe; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.ShapelessRecipe; import net.minecraft.world.level.Level; +import java.util.ArrayList; import java.util.List; -public final class PrintoutRecipe extends CustomRecipe { - private final Ingredient leather; - private final Ingredient string; +/** + * A recipe for combining one or more printed pages together. + *

+ * This behaves similarly to a {@link ShapelessRecipe}, but allows a variable number of pages to appear as ingredients. + * + * @see PrintoutItem + * @see PrintoutData + */ +public final class PrintoutRecipe extends ShapelessRecipe { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + ShapelessRecipeSpec.CODEC.forGetter(PrintoutRecipe::toSpec), + Ingredient.CODEC_NONEMPTY.fieldOf("printout").forGetter(x -> x.printout), + ExtraCodecs.POSITIVE_INT.fieldOf("min_printouts").forGetter(x -> x.minPrintouts) + ).apply(instance, PrintoutRecipe::new)); - public PrintoutRecipe(CraftingBookCategory category) { - super(category); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ShapelessRecipeSpec.STREAM_CODEC, PrintoutRecipe::toSpec, + Ingredient.CONTENTS_STREAM_CODEC, x -> x.printout, + ByteBufCodecs.VAR_INT, x -> x.minPrintouts, + PrintoutRecipe::new + ); - var ingredients = PlatformHelper.get().getRecipeIngredients(); - leather = ingredients.leather(); - string = ingredients.string(); + private final NonNullList ingredients; + private final Ingredient printout; + private final int minPrintouts; + private final ShapelessRecipe innerRecipe; + + private final ItemStack result; + + /** + * Construct a new {@link PrintoutRecipe}. + * + * @param spec The base {@link ShapelessRecipeSpec} for this recipe. + * @param printout The items that will be treated as printed pages. + * @param minPrintouts The minimum number of pages required. + */ + public PrintoutRecipe( + ShapelessRecipeSpec spec, Ingredient printout, int minPrintouts + ) { + // We use the full list of ingredients in the recipe itself, so that it behaves sensibly with recipe mods. + super(spec.properties().group(), spec.properties().category(), spec.result(), concat(spec.ingredients(), printout, minPrintouts)); + + this.ingredients = spec.ingredients(); + this.printout = printout; + this.minPrintouts = minPrintouts; + this.result = spec.result(); + + // However, when testing whether the recipe matches, we only want to use the non-printout ingredients. To do + // that, we create a hidden recipe with the main ingredients. + this.innerRecipe = spec.create(); + } + + private static NonNullList concat(NonNullList first, Ingredient pages, int pagesRequired) { + var result = NonNullList.withSize(first.size() + pagesRequired, Ingredient.EMPTY); + var idx = 0; + for (var ingredient : first) result.set(idx++, ingredient); + for (var i = 0; i < pagesRequired; i++) result.set(idx++, pages); + return result; + } + + private ShapelessRecipeSpec toSpec() { + return new ShapelessRecipeSpec(RecipeProperties.of(this), ingredients, result); } @Override - public boolean canCraftInDimensions(int x, int y) { - return x >= 3 && y >= 3; - } + public boolean matches(CraftingContainer inv, Level world) { + var stackedContents = new StackedContents(); - @Override - public ItemStack getResultItem(HolderLookup.Provider registryAccess) { - return new ItemStack(ModRegistry.Items.PRINTED_PAGES.get()); - } + var inputs = 0; + var printouts = 0; + var pages = 0; + var hasPrintout = false; - @Override - public boolean matches(CraftingContainer inventory, Level world) { - return !assemble(inventory, world.registryAccess()).isEmpty(); - } + for (var j = 0; j < inv.getContainerSize(); ++j) { + var stack = inv.getItem(j); + if (stack.isEmpty()) continue; + if (printout.test(stack)) { + printouts++; - @Override - public ItemStack assemble(CraftingContainer inventory, HolderLookup.Provider registryAccess) { - // See if we match the recipe, and extract the input disk ID and dye colour - var numPages = 0; - var numPrintouts = 0; - ItemStack[] printouts = null; - var stringFound = false; - var leatherFound = false; - var printoutFound = false; - for (var y = 0; y < inventory.getHeight(); y++) { - for (var x = 0; x < inventory.getWidth(); x++) { - var stack = inventory.getItem(x + y * inventory.getWidth()); - if (!stack.isEmpty()) { - if (stack.getItem() instanceof PrintoutItem printout && printout.getType() != PrintoutItem.Type.BOOK) { - if (printouts == null) printouts = new ItemStack[9]; - printouts[numPrintouts] = stack; - numPages += PrintoutData.getOrEmpty(stack).pages(); - numPrintouts++; - printoutFound = true; - } else if (stack.getItem() == Items.PAPER) { - if (printouts == null) { - printouts = new ItemStack[9]; - } - printouts[numPrintouts] = stack; - numPages++; - numPrintouts++; - } else if (string.test(stack) && !stringFound) { - stringFound = true; - } else if (leather.test(stack) && !leatherFound) { - leatherFound = true; - } else { - return ItemStack.EMPTY; - } - } - } - } - - // Build some pages with what was passed in - if (numPages <= PrintoutData.MAX_PAGES && stringFound && printoutFound && numPrintouts >= (leatherFound ? 1 : 2)) { - if (printouts == null) throw new IllegalStateException("Printouts must be non-null"); - var lines = new PrintoutData.Line[numPages * PrintoutData.LINES_PER_PAGE]; - var line = 0; - - for (var printout = 0; printout < numPrintouts; printout++) { - var pageText = printouts[printout].get(ModRegistry.DataComponents.PRINTOUT.get()); - if (pageText != null) { - // Add a printout - for (var pageLine : pageText.lines()) lines[line++] = pageLine; + var printout = stack.get(ModRegistry.DataComponents.PRINTOUT.get()); + if (printout == null) { + pages++; } else { - // Add a blank page - for (var pageLine = 0; pageLine < PrintoutData.LINES_PER_PAGE; pageLine++) { - lines[line++] = PrintoutData.Line.EMPTY; - } + hasPrintout = true; + pages += printout.pages(); } + } else { + inputs++; + stackedContents.accountStack(stack, 1); } - - var title = PrintoutData.getOrEmpty(printouts[0]).title(); - - return DataComponentUtil.createStack( - leatherFound ? ModRegistry.Items.PRINTED_BOOK.get() : ModRegistry.Items.PRINTED_PAGES.get(), - ModRegistry.DataComponents.PRINTOUT.get(), new PrintoutData(title, List.of(lines)) - ); } - return ItemStack.EMPTY; + return hasPrintout && printouts >= minPrintouts && pages <= PrintoutData.MAX_PAGES + && inputs == ingredients.size() && stackedContents.canCraft(innerRecipe, null); + } + + @Override + public ItemStack assemble(CraftingContainer inv, HolderLookup.Provider registries) { + List data = new ArrayList<>(); + for (var j = 0; j < inv.getContainerSize(); ++j) { + var stack = inv.getItem(j); + if (!stack.isEmpty() && printout.test(stack)) data.add(PrintoutData.getOrEmpty(stack)); + } + + if (data.isEmpty()) throw new IllegalStateException("Printouts must be non-null"); + + var lines = data.stream().flatMap(x -> x.lines().stream()).toList(); + + var result = super.assemble(inv, registries); + result.set(ModRegistry.DataComponents.PRINTOUT.get(), new PrintoutData(data.getFirst().title(), lines)); + return result; } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/shared/recipe/ShapelessRecipeSpec.java b/projects/common/src/main/java/dan200/computercraft/shared/recipe/ShapelessRecipeSpec.java index 1d54f4a91..ebf165556 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/recipe/ShapelessRecipeSpec.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/recipe/ShapelessRecipeSpec.java @@ -49,4 +49,13 @@ public record ShapelessRecipeSpec(RecipeProperties properties, NonNullList