1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-09-21 03:39:44 +00:00

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.
This commit is contained in:
Jonathan Coates 2024-05-09 18:47:22 +01:00
parent 2c0d8263d3
commit ad70e2ad90
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
10 changed files with 162 additions and 107 deletions

View File

@ -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"}
}

View File

@ -1 +0,0 @@
{"type": "computercraft:printout", "category": "misc"}

View File

@ -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);
}

View File

@ -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<ShapelessS
}
public FinishedRecipe build() {
return build(spec -> new ShapelessRecipe(spec.properties().group(), spec.properties().category(), spec.result(), spec.ingredients()));
return build(ShapelessRecipeSpec::create);
}
}

View File

@ -249,12 +249,16 @@ public final class ModRegistry {
public static final RegistryEntry<TreasureDiskItem> 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<PrintoutItem> PRINTED_PAGE = REGISTRY.register("printed_page",
() -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGE));
() -> new PrintoutItem(printoutProperties(), PrintoutItem.Type.PAGE));
public static final RegistryEntry<PrintoutItem> PRINTED_PAGES = REGISTRY.register("printed_pages",
() -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.PAGES));
() -> new PrintoutItem(printoutProperties(), PrintoutItem.Type.PAGES));
public static final RegistryEntry<PrintoutItem> PRINTED_BOOK = REGISTRY.register("printed_book",
() -> new PrintoutItem(properties().stacksTo(1), PrintoutItem.Type.BOOK));
() -> new PrintoutItem(printoutProperties(), PrintoutItem.Type.BOOK));
public static final RegistryEntry<BlockItem> SPEAKER = ofBlock(Blocks.SPEAKER, BlockItem::new);
public static final RegistryEntry<BlockItem> DISK_DRIVE = ofBlock(Blocks.DISK_DRIVE, BlockItem::new);
@ -488,7 +492,7 @@ public final class ModRegistry {
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<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<RecipeSerializer<PrintoutRecipe>> PRINTOUT = register("printout", PrintoutRecipe.CODEC, PrintoutRecipe.STREAM_CODEC);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> 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();

View File

@ -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.
* <p>
* 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<PrintoutRecipe> 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);
var ingredients = PlatformHelper.get().getRecipeIngredients();
leather = ingredients.leather();
string = ingredients.string();
}
@Override
public boolean canCraftInDimensions(int x, int y) {
return x >= 3 && y >= 3;
}
@Override
public ItemStack getResultItem(HolderLookup.Provider registryAccess) {
return new ItemStack(ModRegistry.Items.PRINTED_PAGES.get());
}
@Override
public boolean matches(CraftingContainer inventory, Level world) {
return !assemble(inventory, world.registryAccess()).isEmpty();
}
@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;
} else {
// Add a blank page
for (var pageLine = 0; pageLine < PrintoutData.LINES_PER_PAGE; pageLine++) {
lines[line++] = PrintoutData.Line.EMPTY;
}
}
}
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))
public static final StreamCodec<RegistryFriendlyByteBuf, PrintoutRecipe> STREAM_CODEC = StreamCodec.composite(
ShapelessRecipeSpec.STREAM_CODEC, PrintoutRecipe::toSpec,
Ingredient.CONTENTS_STREAM_CODEC, x -> x.printout,
ByteBufCodecs.VAR_INT, x -> x.minPrintouts,
PrintoutRecipe::new
);
private final NonNullList<Ingredient> 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();
}
return ItemStack.EMPTY;
private static NonNullList<Ingredient> concat(NonNullList<Ingredient> 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 matches(CraftingContainer inv, Level world) {
var stackedContents = new StackedContents();
var inputs = 0;
var printouts = 0;
var pages = 0;
var hasPrintout = false;
for (var j = 0; j < inv.getContainerSize(); ++j) {
var stack = inv.getItem(j);
if (stack.isEmpty()) continue;
if (printout.test(stack)) {
printouts++;
var printout = stack.get(ModRegistry.DataComponents.PRINTOUT.get());
if (printout == null) {
pages++;
} else {
hasPrintout = true;
pages += printout.pages();
}
} else {
inputs++;
stackedContents.accountStack(stack, 1);
}
}
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<PrintoutData> 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

View File

@ -49,4 +49,13 @@ public record ShapelessRecipeSpec(RecipeProperties properties, NonNullList<Ingre
ItemStack.STREAM_CODEC, ShapelessRecipeSpec::result,
ShapelessRecipeSpec::new
);
/**
* Create a basic {@link ShapelessRecipe} from this spec.
*
* @return The newly constructed recipe.
*/
public ShapelessRecipe create() {
return new ShapelessRecipe(properties().group(), properties().category(), result(), ingredients());
}
}

View File

@ -130,6 +130,7 @@ loom {
runs {
configureEach {
ideConfigGenerated(true)
property("fabric-tag-conventions-v2.missingTagTranslationWarning", "VERBOSE")
}
named("client") {

View File

@ -1,6 +1,12 @@
{
"type": "computercraft:impostor_shapeless",
"type": "computercraft:printout",
"category": "redstone",
"ingredients": [{"item": "minecraft:leather"}, {"item": "computercraft:printed_page"}, {"tag": "c:strings"}],
"ingredients": [{"item": "minecraft:leather"}, {"tag": "c:strings"}],
"min_printouts": 1,
"printout": [
{"item": "computercraft:printed_page"},
{"item": "computercraft:printed_pages"},
{"item": "minecraft:paper"}
],
"result": {"count": 1, "id": "computercraft:printed_book"}
}

View File

@ -1,6 +1,12 @@
{
"type": "computercraft:impostor_shapeless",
"type": "computercraft:printout",
"category": "redstone",
"ingredients": [{"tag": "c:leathers"}, {"item": "computercraft:printed_page"}, {"tag": "c:strings"}],
"ingredients": [{"tag": "c:leathers"}, {"tag": "c:strings"}],
"min_printouts": 1,
"printout": [
{"item": "computercraft:printed_page"},
{"item": "computercraft:printed_pages"},
{"item": "minecraft:paper"}
],
"result": {"count": 1, "id": "computercraft:printed_book"}
}