From 11d1de9075c8f7a57404c85bcf96a4fa64ca4f42 Mon Sep 17 00:00:00 2001 From: 90 Date: Fri, 27 Dec 2024 22:11:37 +0000 Subject: [PATCH] Make various more improvements to bulk compression - Allow for compression/decompression recipes with amounts other than 4 and 9 (e.g. certain mods' "Tiny Coal/Charcoal") - Add a new "compression blacklist" tag with which to prevent certain items from ever appearing in a compression chain, override or not - Refine compression recipe logic in order to better catch other odd cases such as mods with atypical ingot/block recipes for their own metals (Closes #154, but does assume the presence of some unification mod such as AlmostUnified) - Add better, more detailed logging for compression including debug logs for every individually generated chain and override - Add some more default overrides such as string, snow and honeycomb --- build.gradle.kts | 1 + settings.gradle.kts | 2 +- .../megacells/datagen/MEGADataGenerators.java | 5 +- .../megacells/datagen/MEGATagProvider.java | 35 ++++-- .../630870a35b22409c88cc832755e7bd651582eeae | 5 +- .../tags/item/compression_blacklist.json | 13 +++ .../tags/item/compression_overrides.json | 4 + .../java/gripe/_90/megacells/MEGACells.java | 5 - .../_90/megacells/definition/MEGATags.java | 1 + .../_90/megacells/item/part/CellDockPart.java | 8 +- .../_90/megacells/misc/CompressionChain.java | 5 + .../megacells/misc/CompressionService.java | 106 ++++++++++++------ 12 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 src/generated/resources/data/megacells/tags/item/compression_blacklist.json diff --git a/build.gradle.kts b/build.gradle.kts index 660ca175..560b0989 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,7 @@ neoForge { runs { configureEach { + logLevel = org.slf4j.event.Level.DEBUG gameDirectory = file("run") } diff --git a/settings.gradle.kts b/settings.gradle.kts index b7a0f3e6..96dd9b7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -87,7 +87,7 @@ run { version("minecraft", mc) val nf = mc.substringAfter('.') - version("neoforge", "${nf + (if (!nf.contains('.')) ".0" else "")}.66") + version("neoforge", "${nf + (if (!nf.contains('.')) ".0" else "")}.91") version("parchment", "2024.07.28") version("ae2", "19.1.1-beta") diff --git a/src/data/java/gripe/_90/megacells/datagen/MEGADataGenerators.java b/src/data/java/gripe/_90/megacells/datagen/MEGADataGenerators.java index cbb7c46b..4294e1e3 100644 --- a/src/data/java/gripe/_90/megacells/datagen/MEGADataGenerators.java +++ b/src/data/java/gripe/_90/megacells/datagen/MEGADataGenerators.java @@ -7,7 +7,6 @@ import gripe._90.megacells.MEGACells; -@SuppressWarnings("unused") @EventBusSubscriber(modid = MEGACells.MODID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public class MEGADataGenerators { @SubscribeEvent @@ -24,11 +23,11 @@ public static void onGatherData(GatherDataEvent event) { generator.addProvider(event.includeServer(), new MEGARecipeProvider(output, registries)); generator.addProvider(event.includeServer(), new MEGALootProvider(output, registries)); - var blockTags = new MEGATagProvider.BlockTags(output, registries, existing); + var blockTags = new MEGATagProvider.Block(output, registries, existing); generator.addProvider(event.includeServer(), blockTags); generator.addProvider( event.includeServer(), - new MEGATagProvider.ItemTags(output, registries, blockTags.contentsGetter(), existing)); + new MEGATagProvider.Item(output, registries, blockTags.contentsGetter(), existing)); generator.addProvider( event.includeClient(), diff --git a/src/data/java/gripe/_90/megacells/datagen/MEGATagProvider.java b/src/data/java/gripe/_90/megacells/datagen/MEGATagProvider.java index d09c6da2..18bb0e51 100644 --- a/src/data/java/gripe/_90/megacells/datagen/MEGATagProvider.java +++ b/src/data/java/gripe/_90/megacells/datagen/MEGATagProvider.java @@ -2,6 +2,7 @@ import java.util.concurrent.CompletableFuture; +import appeng.datagen.providers.tags.ConventionTags; import org.jetbrains.annotations.NotNull; import net.minecraft.core.HolderLookup; @@ -12,9 +13,9 @@ import net.minecraft.data.tags.ItemTagsProvider; import net.minecraft.data.tags.TagsProvider; import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.BlockTags; import net.minecraft.tags.TagKey; import net.minecraft.world.item.Items; -import net.minecraft.world.level.block.Block; import net.neoforged.neoforge.common.Tags; import net.neoforged.neoforge.common.data.ExistingFileHelper; @@ -26,8 +27,8 @@ import gripe._90.megacells.definition.MEGATags; public class MEGATagProvider { - public static class BlockTags extends IntrinsicHolderTagsProvider { - public BlockTags( + public static class Block extends IntrinsicHolderTagsProvider { + public Block( PackOutput output, CompletableFuture registries, ExistingFileHelper existing) { super( output, @@ -41,7 +42,7 @@ public BlockTags( @Override protected void addTags(@NotNull HolderLookup.Provider provider) { for (var block : MEGABlocks.getBlocks()) { - tag(net.minecraft.tags.BlockTags.MINEABLE_WITH_PICKAXE).add(block.block()); + tag(BlockTags.MINEABLE_WITH_PICKAXE).add(block.block()); } tag(MEGATags.SKY_STEEL_BLOCK).add(MEGABlocks.SKY_STEEL_BLOCK.block()); @@ -61,20 +62,20 @@ public String getName() { } } - public static class ItemTags extends ItemTagsProvider { - public ItemTags( + public static class Item extends ItemTagsProvider { + public Item( PackOutput output, CompletableFuture registries, - CompletableFuture> blockTags, + CompletableFuture> blockTags, ExistingFileHelper existing) { super(output, registries, blockTags, MEGACells.MODID, existing); } @Override protected void addTags(@NotNull HolderLookup.Provider provider) { - copy(MEGATags.SKY_STEEL_BLOCK, TagKey.create(Registries.ITEM, MEGATags.SKY_STEEL_BLOCK.location())); - copy(MEGATags.SKY_BRONZE_BLOCK, TagKey.create(Registries.ITEM, MEGATags.SKY_BRONZE_BLOCK.location())); - copy(MEGATags.SKY_OSMIUM_BLOCK, TagKey.create(Registries.ITEM, MEGATags.SKY_OSMIUM_BLOCK.location())); + copy(MEGATags.SKY_STEEL_BLOCK); + copy(MEGATags.SKY_BRONZE_BLOCK); + copy(MEGATags.SKY_OSMIUM_BLOCK); tag(MEGATags.SKY_STEEL_INGOT).add(MEGAItems.SKY_STEEL_INGOT.asItem()); tag(MEGATags.SKY_BRONZE_INGOT).add(MEGAItems.SKY_BRONZE_INGOT.asItem()); @@ -95,9 +96,19 @@ protected void addTags(@NotNull HolderLookup.Provider provider) { .add(Items.CLAY_BALL) .add(Items.MELON_SLICE) .add(Items.ICE, Items.PACKED_ICE) + .add(Items.STRING) + .add(Items.SNOWBALL) + .add(Items.HONEYCOMB) + .add(Items.POINTED_DRIPSTONE) .addOptionalTag( ResourceLocation.fromNamespaceAndPath("functionalstorage", "ignore_crafting_check")); + tag(MEGATags.COMPRESSION_BLACKLIST) + .addTag(Tags.Items.SEEDS) + .addTag(ConventionTags.WRENCH) + .addOptionalTag(ResourceLocation.fromNamespaceAndPath("mysticalagriculture", "essences")) + .remove(ResourceLocation.fromNamespaceAndPath("mysticalagriculture", "inferium_essence")); + tag(Tags.Items.INGOTS) .addTag(MEGATags.SKY_STEEL_INGOT) .addTag(MEGATags.SKY_BRONZE_INGOT) @@ -105,6 +116,10 @@ protected void addTags(@NotNull HolderLookup.Provider provider) { copy(Tags.Blocks.STORAGE_BLOCKS, Tags.Items.STORAGE_BLOCKS); } + private void copy(TagKey blockTag) { + copy(blockTag, TagKey.create(Registries.ITEM, blockTag.location())); + } + @NotNull @Override public String getName() { diff --git a/src/generated/resources/.cache/630870a35b22409c88cc832755e7bd651582eeae b/src/generated/resources/.cache/630870a35b22409c88cc832755e7bd651582eeae index 8904ca12..5dcc0b43 100644 --- a/src/generated/resources/.cache/630870a35b22409c88cc832755e7bd651582eeae +++ b/src/generated/resources/.cache/630870a35b22409c88cc832755e7bd651582eeae @@ -1,4 +1,4 @@ -// 1.21.1 2024-08-25T13:50:12.498501175 Tags (Item) +// 1.21.1 2024-12-27T14:46:27.46760465 Tags (Item) e42fb28c86b642b8733975efead834402f7fe45b data/ae2/tags/item/p2p_attunements/fe_p2p_tunnel.json bf572a87643cf277b91bffb416e388a94cd36747 data/c/tags/item/ingots.json 61e4876c9c48d312a89177d6a5c81319297777ac data/c/tags/item/ingots/sky_bronze.json @@ -8,6 +8,7 @@ bf572a87643cf277b91bffb416e388a94cd36747 data/c/tags/item/ingots.json d16fe455331319a8d365af59139ac5d88a119144 data/c/tags/item/storage_blocks/sky_bronze.json 7a65c757f9501fd29bc86bebcbae0d91f197ff43 data/c/tags/item/storage_blocks/sky_osmium.json 1bfe8421078982de39dc3cb1d5917d6385175b44 data/c/tags/item/storage_blocks/sky_steel.json -99c21c9a0d2d3ed906e8580c0e1752315f114ba6 data/megacells/tags/item/compression_overrides.json +2cb7c155468ad5c415e6b2722c64f359b70b1a98 data/megacells/tags/item/compression_blacklist.json +685f7cac39a1df27bd3a919f3a2ef841b85a4c87 data/megacells/tags/item/compression_overrides.json 6945345eddaa46dddeb49da1543ec0850f03278a data/megacells/tags/item/mega_interface.json 75a9b0d69567961cd99e9e16fc2f9d57a8101f82 data/megacells/tags/item/mega_pattern_provider.json diff --git a/src/generated/resources/data/megacells/tags/item/compression_blacklist.json b/src/generated/resources/data/megacells/tags/item/compression_blacklist.json new file mode 100644 index 00000000..4b7e6798 --- /dev/null +++ b/src/generated/resources/data/megacells/tags/item/compression_blacklist.json @@ -0,0 +1,13 @@ +{ + "remove": [ + "mysticalagriculture:inferium_essence" + ], + "values": [ + "#c:seeds", + "#c:tools/wrench", + { + "id": "#mysticalagriculture:essences", + "required": false + } + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/megacells/tags/item/compression_overrides.json b/src/generated/resources/data/megacells/tags/item/compression_overrides.json index 50e81c49..01e6f633 100644 --- a/src/generated/resources/data/megacells/tags/item/compression_overrides.json +++ b/src/generated/resources/data/megacells/tags/item/compression_overrides.json @@ -8,6 +8,10 @@ "minecraft:melon_slice", "minecraft:ice", "minecraft:packed_ice", + "minecraft:string", + "minecraft:snowball", + "minecraft:honeycomb", + "minecraft:pointed_dripstone", { "id": "#functionalstorage:ignore_crafting_check", "required": false diff --git a/src/main/java/gripe/_90/megacells/MEGACells.java b/src/main/java/gripe/_90/megacells/MEGACells.java index 654b54d1..ff46edef 100644 --- a/src/main/java/gripe/_90/megacells/MEGACells.java +++ b/src/main/java/gripe/_90/megacells/MEGACells.java @@ -2,9 +2,6 @@ import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.item.ItemStack; @@ -42,7 +39,6 @@ import gripe._90.megacells.definition.MEGACreativeTab; import gripe._90.megacells.definition.MEGAItems; import gripe._90.megacells.definition.MEGAMenus; -import gripe._90.megacells.definition.MEGATranslations; import gripe._90.megacells.integration.Addons; import gripe._90.megacells.integration.appmek.RadioactiveCellItem; import gripe._90.megacells.item.cell.BulkCellItem; @@ -51,7 +47,6 @@ @Mod(MEGACells.MODID) public class MEGACells { public static final String MODID = "megacells"; - public static final Logger LOGGER = LoggerFactory.getLogger(MEGATranslations.ModName.getEnglishText()); public MEGACells(ModContainer container, IEventBus eventBus) { MEGABlocks.DR.register(eventBus); diff --git a/src/main/java/gripe/_90/megacells/definition/MEGATags.java b/src/main/java/gripe/_90/megacells/definition/MEGATags.java index e93ae7b8..8359ea7b 100644 --- a/src/main/java/gripe/_90/megacells/definition/MEGATags.java +++ b/src/main/java/gripe/_90/megacells/definition/MEGATags.java @@ -25,4 +25,5 @@ public final class MEGATags { public static final TagKey MEGA_PATTERN_PROVIDER = ItemTags.create(MEGABlocks.MEGA_PATTERN_PROVIDER.id()); public static final TagKey COMPRESSION_OVERRIDES = ItemTags.create(MEGACells.makeId("compression_overrides")); + public static final TagKey COMPRESSION_BLACKLIST = ItemTags.create(MEGACells.makeId("compression_blacklist")); } diff --git a/src/main/java/gripe/_90/megacells/item/part/CellDockPart.java b/src/main/java/gripe/_90/megacells/item/part/CellDockPart.java index 5c06b656..fa3ac886 100644 --- a/src/main/java/gripe/_90/megacells/item/part/CellDockPart.java +++ b/src/main/java/gripe/_90/megacells/item/part/CellDockPart.java @@ -5,6 +5,8 @@ import com.mojang.blaze3d.vertex.PoseStack; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.MultiBufferSource; @@ -63,6 +65,8 @@ public class CellDockPart extends AEBasePart implements InternalInventoryHost, IChestOrDrive, IStorageProvider, IPriorityHost { + private static final Logger LOGGER = LoggerFactory.getLogger(CellDockPart.class); + @PartModels private static final IPartModel MODEL = new PartModel(MEGACells.makeId("part/cell_dock")); @@ -127,14 +131,14 @@ public void readVisualStateFromNBT(CompoundTag data) { try { clientCell = BuiltInRegistries.ITEM.get(ResourceLocation.parse(data.getString("cellId"))); } catch (Exception e) { - MEGACells.LOGGER.warn("Couldn't read cell item for {} from {}", this, data); + LOGGER.warn("Couldn't read cell item for {} from {}", this, data); clientCell = Items.AIR; } try { clientCellState = CellState.valueOf(data.getString("cellStatus")); } catch (Exception e) { - MEGACells.LOGGER.warn("Couldn't read cell status for {} from {}", this, data); + LOGGER.warn("Couldn't read cell status for {} from {}", this, data); clientCellState = CellState.ABSENT; } } diff --git a/src/main/java/gripe/_90/megacells/misc/CompressionChain.java b/src/main/java/gripe/_90/megacells/misc/CompressionChain.java index ed57bb7d..c26b9380 100644 --- a/src/main/java/gripe/_90/megacells/misc/CompressionChain.java +++ b/src/main/java/gripe/_90/megacells/misc/CompressionChain.java @@ -55,5 +55,10 @@ public record Variant(AEItemKey item, int factor) { Variant(Item item, int factor) { this(AEItemKey.of(item), factor); } + + @Override + public String toString() { + return factor + "x → " + item; + } } } diff --git a/src/main/java/gripe/_90/megacells/misc/CompressionService.java b/src/main/java/gripe/_90/megacells/misc/CompressionService.java index 15108480..5c55f2b8 100644 --- a/src/main/java/gripe/_90/megacells/misc/CompressionService.java +++ b/src/main/java/gripe/_90/megacells/misc/CompressionService.java @@ -10,14 +10,15 @@ import java.util.Set; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.minecraft.core.RegistryAccess; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.CraftingRecipe; -import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeType; -import net.minecraft.world.item.crafting.ShapedRecipe; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.OnDatapackSyncEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent; @@ -25,14 +26,15 @@ import appeng.api.networking.GridServices; import appeng.api.stacks.AEItemKey; -import gripe._90.megacells.MEGACells; import gripe._90.megacells.definition.MEGATags; public class CompressionService { + private static final Logger LOGGER = LoggerFactory.getLogger(CompressionService.class); + // Each chain is a list of "variants", where each variant consists of the item itself along with an associated value // dictating how much of the previous variant's item is needed to compress into that variant. - // This value is typically either 4 or 9 for any given item, or 1 for the smallest base variant. - private static final Set compressionChains = new HashSet<>(); + // This value will be between 1 and 9 (inclusive) for any given item, or just 1 for the smallest base variant. + private static final Set chains = new HashSet<>(); // It may be desirable for some items to be included as variants in a chain in spite of any recipes involving those // items not being reversible. Hence, we override any reversibility checks and generate a variant for such an item @@ -40,9 +42,7 @@ public class CompressionService { private static final Set overrides = new HashSet<>(); public static Optional getChain(AEItemKey item) { - return compressionChains.stream() - .filter(chain -> chain.containsVariant(item)) - .findFirst(); + return chains.stream().filter(chain -> chain.containsVariant(item)).findFirst(); } public static void init() { @@ -64,7 +64,7 @@ public static void init() { private static void loadRecipes(RecipeManager recipeManager, RegistryAccess access) { // Clear old chain cache in case of the server restarting or recipes being reloaded - compressionChains.clear(); + chains.clear(); overrides.clear(); // Retrieve all available "compression" and "decompression" recipes from the current server's recipe manager @@ -74,7 +74,7 @@ private static void loadRecipes(RecipeManager recipeManager, RegistryAccess acce for (var recipe : recipeManager.getAllRecipesFor(RecipeType.CRAFTING)) { if (isCompressionRecipe(recipe.value(), access)) { compressed.add(recipe.value()); - } else if (isDecompressionRecipe(recipe.value(), access)) { + } else if (isDecompressionRecipe(recipe.value())) { decompressed.add(recipe.value()); } } @@ -94,9 +94,11 @@ private static void loadRecipes(RecipeManager recipeManager, RegistryAccess acce var baseVariant = recipe.getResultItem(access).getItem(); if (getChain(AEItemKey.of(baseVariant)).isEmpty()) { - compressionChains.add(generateChain(baseVariant, compressed, decompressed, access)); + chains.add(generateChain(baseVariant, compressed, decompressed, access)); } }); + + LOGGER.info("Initialised bulk compression. {} compression chains gathered.", chains.size()); } private static CompressionChain generateChain( @@ -113,9 +115,12 @@ private static CompressionChain generateChain( var item = lower.item().getItem(); if (variants.contains(item)) { - MEGACells.LOGGER.warn( - "Duplicate lower compression variant detected: {}. Check any recipe involving this item for problems.", - lower); + if (lower.factor() != 1) { + LOGGER.warn( + "Duplicate lower compression variant detected: {}. Check any recipe involving this item for problems.", + lower); + } + break; } @@ -133,9 +138,12 @@ private static CompressionChain generateChain( for (var higher = getNextVariant(baseVariant, compressed, true, access); higher != null; ) { if (chain.contains(higher)) { - MEGACells.LOGGER.warn( - "Duplicate higher compression variant detected: {}. Check any recipe involving this item for problems.", - higher); + if (higher.factor() != 1) { + LOGGER.warn( + "Duplicate higher compression variant detected: {}. Check any recipe involving this item for problems.", + higher); + } + break; } @@ -143,6 +151,7 @@ private static CompressionChain generateChain( higher = getNextVariant(higher.item().getItem(), compressed, true, access); } + LOGGER.debug("Gathered bulk compression chain: {}", chain); return chain; } @@ -173,27 +182,29 @@ private static CompressionChain.Variant getNextVariant( return null; } - private static boolean isDecompressionRecipe(CraftingRecipe recipe, RegistryAccess access) { - return recipe.getIngredients().stream().filter(i -> !i.isEmpty()).count() == 1 - && Set.of(4, 9).contains(recipe.getResultItem(access).getCount()); + private static boolean isDecompressionRecipe(CraftingRecipe recipe) { + return recipe.getIngredients().stream().filter(i -> !i.isEmpty()).count() == 1; } private static boolean isCompressionRecipe(CraftingRecipe recipe, RegistryAccess access) { - var ingredients = recipe.getIngredients(); - return recipe.getResultItem(access).getCount() == 1 - && ingredients.stream().noneMatch(Ingredient::isEmpty) - && Set.of(4, 9).contains(ingredients.size()) - && sameIngredient(recipe); - } + if (recipe.getResultItem(access).getCount() != 1) { + return false; + } - private static boolean sameIngredient(CraftingRecipe recipe) { - var ingredients = recipe.getIngredients(); + var ingredients = recipe.getIngredients().stream() + .filter(i -> !i.isEmpty()) + .distinct() + .toList(); - if (recipe instanceof ShapedRecipe) { - return ingredients.stream().distinct().count() <= 1; + if (ingredients.isEmpty()) { + return false; } - // Check further for any odd cases (e.g. melon blocks having a shapeless recipe instead of a shaped one) + if (ingredients.size() == 1) { + return true; + } + + // Check further for any odd cases such as certain mods' metal ingot/block recipes post-unification var first = ingredients.getFirst().getItems(); for (var ingredient : ingredients) { @@ -226,10 +237,15 @@ private static boolean isIrreversible( var input = candidate.getIngredients().getFirst().getItems(); var output = candidate.getResultItem(access).getItem(); - var compressible = Arrays.stream(input).anyMatch(i -> i.is(testOutput)); - var decompressible = Arrays.stream(testInput).anyMatch(i -> i.is(output)); + // spotless:off + var compressible = Arrays.stream(input).anyMatch(i -> i.is(testOutput) && !i.is(MEGATags.COMPRESSION_BLACKLIST)); + var decompressible = Arrays.stream(testInput).anyMatch(i -> i.is(output) && !i.is(MEGATags.COMPRESSION_BLACKLIST)); + + var sameQuantity = candidate.getResultItem(access).getCount() == recipe.getIngredients().size() + && recipe.getResultItem(access).getCount() == candidate.getIngredients().size(); + // spotless:on - if (compressible && decompressible) { + if (compressible && decompressible && sameQuantity) { return false; } } @@ -239,15 +255,26 @@ private static boolean isIrreversible( private static boolean overrideRecipe(CraftingRecipe recipe, RegistryAccess access) { for (var input : recipe.getIngredients().getFirst().getItems()) { + if (input.is(MEGATags.COMPRESSION_BLACKLIST)) { + return false; + } + if (input.is(MEGATags.COMPRESSION_OVERRIDES)) { - var decompressed = isDecompressionRecipe(recipe, access); var output = recipe.getResultItem(access); + if (output.is(MEGATags.COMPRESSION_BLACKLIST)) { + return false; + } + + var decompressed = isDecompressionRecipe(recipe); var smaller = (decompressed ? output : input).getItem(); var larger = (decompressed ? input : output).getItem(); var factor = !decompressed ? recipe.getIngredients().size() : output.getCount(); - overrides.add(new Override(smaller, larger, factor)); + var override = new Override(smaller, larger, factor); + LOGGER.debug("Found bulk compression override: {}", override); + overrides.add(override); + return true; } } @@ -255,5 +282,10 @@ private static boolean overrideRecipe(CraftingRecipe recipe, RegistryAccess acce return false; } - private record Override(Item smaller, Item larger, int factor) {} + private record Override(Item smaller, Item larger, int factor) { + @java.lang.Override + public String toString() { + return larger + " → " + factor + "x " + smaller; + } + } }