From 710bb57cc3a775e6c1656dd1b002ee31c188ea95 Mon Sep 17 00:00:00 2001 From: brokestar233 <3765589194@qq.com> Date: Tue, 7 Jul 2026 18:15:55 +0800 Subject: [PATCH 1/2] Improved clipboard integration and added EMI recipe tree support Signed-off-by: brokestar233 <3765589194@qq.com> --- .../clipboard/ClipboardScreenUtils.java | 118 +++++++++++++----- .../clipboard/ClipboardTreeRecipe.java | 63 ++++++++++ .../mixin/BoMScreenMixin.java | 41 ++++++ .../mixin/ClipboardScreenMixin.java | 4 +- .../mixin/EmiFavoritesMixin.java | 22 ++++ .../emi_create_schematics/lang/en_us.json | 8 +- .../emi_create_schematics/lang/it_it.json | 8 +- .../emi_create_schematics.mixins.json | 4 +- 8 files changed, 233 insertions(+), 35 deletions(-) create mode 100644 src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java create mode 100644 src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java create mode 100644 src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java diff --git a/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardScreenUtils.java b/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardScreenUtils.java index 2ded86c..3b5ac8e 100644 --- a/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardScreenUtils.java +++ b/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardScreenUtils.java @@ -1,10 +1,16 @@ package net.liukrast.schematicdisplay.clipboard; +import com.simibubi.create.AllBlocks; import com.simibubi.create.content.equipment.clipboard.ClipboardEntry; +import dev.emi.emi.api.EmiApi; +import dev.emi.emi.api.stack.Comparison; import dev.emi.emi.api.stack.EmiStack; -import dev.emi.emi.runtime.EmiFavorite; -import dev.emi.emi.runtime.EmiFavorites; +import dev.emi.emi.bom.BoM; +import net.liukrast.schematicdisplay.EMICreateSchematics; +import net.minecraft.client.Minecraft; import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.CustomData; @@ -14,37 +20,93 @@ public final class ClipboardScreenUtils { public static final String REAL_COUNT_KEY = "CreateEMISchematicsData"; + public static final String VIRTUAL_RECIPE_KEY = "CreateEMISchematicsVirtualRecipe"; + private static long emi_create_schematics$recipeCounter = 0; + private ClipboardScreenUtils() {} public static void load(List> pages) { - List allEntries = new ArrayList<>(); - for(var page : pages) - allEntries.addAll(page); - allEntries.sort(Comparator.comparingInt((ClipboardEntry entry) -> entry.itemAmount).reversed()); - - var copiedList = new ArrayList<>(EmiFavorites.favorites); - - copiedList.stream() - .filter(fav -> { - if(!(fav.getStack() instanceof EmiStack stack)) return false; - var data = stack.getItemStack().getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY); - - if(data.isEmpty()) return false; - return data.contains(REAL_COUNT_KEY); - }) - .forEach(EmiFavorites::removeFavorite); - - for(var entry : allEntries) { - ItemStack stack1 = entry.icon.copy(); - stack1.update(DataComponents.CUSTOM_DATA, CustomData.EMPTY, existingData -> - existingData.update(tag -> tag.putLong(REAL_COUNT_KEY, entry.itemAmount)) - ); - EmiStack emiStack = EmiStack.of(stack1); - emiStack.setAmount(1); + List remainingEntries = collectRemainingEntries(pages); + if (remainingEntries.isEmpty()) { + notifyEmptyTree(); + return; + } + + List inputs = new ArrayList<>(remainingEntries.size()); + for (EntryAmount entry : remainingEntries) { + inputs.add(EmiStack.of(entry.stack.copy()).setAmount(entry.amount)); + } + + ClipboardTreeRecipe recipe = new ClipboardTreeRecipe( + inputs, + createVirtualOutput(), + nextVirtualRecipeId() + ); + + BoM.setGoal(recipe); + BoM.craftingMode = true; + + Minecraft minecraft = Minecraft.getInstance(); + minecraft.setScreen(null); + EmiApi.viewRecipeTree(); + } + + private static List collectRemainingEntries(List> pages) { + List mergedEntries = new ArrayList<>(); + for (List page : pages) { + for (ClipboardEntry entry : page) { + if (entry.checked || entry.icon.isEmpty() || entry.itemAmount <= 0) { + continue; + } + mergeEntry(mergedEntries, entry.icon, entry.itemAmount); + } + } + mergedEntries.sort(Comparator.comparingLong((EntryAmount entry) -> entry.amount).reversed()); + return mergedEntries; + } + + private static void mergeEntry(List mergedEntries, ItemStack stack, long amount) { + for (EntryAmount entry : mergedEntries) { + if (ItemStack.isSameItemSameComponents(entry.stack, stack)) { + entry.amount += amount; + return; + } + } + mergedEntries.add(new EntryAmount(stack.copy(), amount)); + } + + private static EmiStack createVirtualOutput() { + ItemStack stack = AllBlocks.CLIPBOARD.asStack(); + stack.set(DataComponents.CUSTOM_NAME, Component.translatable("gui." + EMICreateSchematics.MOD_ID + ".clipboard.tree_output")); + stack.update(DataComponents.CUSTOM_DATA, CustomData.EMPTY, + existingData -> existingData.update(tag -> tag.putBoolean(VIRTUAL_RECIPE_KEY, true))); + return EmiStack.of(stack).comparison(Comparison.compareComponents()); + } + private static ResourceLocation nextVirtualRecipeId() { + return ResourceLocation.fromNamespaceAndPath( + EMICreateSchematics.MOD_ID, + "/clipboard/" + emi_create_schematics$recipeCounter++ + ); + } + + private static void notifyEmptyTree() { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.player != null) { + minecraft.player.displayClientMessage( + Component.translatable("gui." + EMICreateSchematics.MOD_ID + ".clipboard.tree_empty"), + true + ); + } + } + private static final class EntryAmount { + private final ItemStack stack; + private long amount; - if(!entry.checked) EmiFavorites.addFavorite(emiStack); + private EntryAmount(ItemStack stack, long amount) { + this.stack = stack; + this.amount = amount; } } -} \ No newline at end of file +} diff --git a/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java b/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java new file mode 100644 index 0000000..42a5700 --- /dev/null +++ b/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java @@ -0,0 +1,63 @@ +package net.liukrast.schematicdisplay.clipboard; + +import dev.emi.emi.api.recipe.BasicEmiRecipe; +import dev.emi.emi.api.recipe.EmiRecipe; +import dev.emi.emi.api.recipe.VanillaEmiRecipeCategories; +import dev.emi.emi.api.render.EmiTexture; +import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.api.widget.WidgetHolder; +import net.liukrast.schematicdisplay.EMICreateSchematics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +public final class ClipboardTreeRecipe extends BasicEmiRecipe { + private static final int DISPLAYED_INPUTS = 9; + private final Component overflowLabel; + + public ClipboardTreeRecipe(List inputs, EmiStack output, ResourceLocation id) { + super(VanillaEmiRecipeCategories.CRAFTING, id, 118, 72); + this.inputs.addAll(inputs); + this.outputs.add(output); + + int hiddenInputs = Math.max(0, inputs.size() - DISPLAYED_INPUTS); + overflowLabel = hiddenInputs > 0 + ? Component.translatable("gui." + EMICreateSchematics.MOD_ID + ".clipboard.tree_more", hiddenInputs) + : Component.empty(); + } + + public static boolean isVirtual(EmiRecipe recipe) { + return recipe instanceof ClipboardTreeRecipe; + } + + @Override + public boolean hideCraftable() { + return true; + } + + @Override + public void addWidgets(WidgetHolder widgets) { + widgets.addText( + Component.translatable("gui." + EMICreateSchematics.MOD_ID + ".clipboard.tree_recipe"), + 0, + 0, + 0x404040, + false + ); + widgets.addTexture(EmiTexture.EMPTY_ARROW, 60, 22); + + for (int i = 0; i < DISPLAYED_INPUTS; i++) { + if (i < inputs.size()) { + widgets.addSlot(inputs.get(i), i % 3 * 18, 12 + i / 3 * 18); + } else { + widgets.addSlot(i % 3 * 18, 12 + i / 3 * 18); + } + } + + widgets.addSlot(outputs.get(0), 92, 18).large(true).recipeContext(this); + if (!overflowLabel.getString().isEmpty()) { + widgets.addText(overflowLabel, 0, 62, 0x787878, false); + } + } +} diff --git a/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java b/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java new file mode 100644 index 0000000..9770fbe --- /dev/null +++ b/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java @@ -0,0 +1,41 @@ +package net.liukrast.schematicdisplay.mixin; + +import dev.emi.emi.bom.BoM; +import dev.emi.emi.bom.MaterialNode; +import dev.emi.emi.bom.ProgressState; +import dev.emi.emi.screen.BoMScreen; +import net.liukrast.schematicdisplay.clipboard.ClipboardTreeRecipe; +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.CallbackInfo; + +@Mixin(BoMScreen.class) +public abstract class BoMScreenMixin { + @Inject(method = "recalculateTree", at = @At("TAIL"), remap = false) + private void emi_create_schematics$markVirtualRootProgress(CallbackInfo ci) { + if (BoM.tree == null || !ClipboardTreeRecipe.isVirtual(BoM.tree.goal.recipe) || BoM.tree.goal.children == null) { + return; + } + + boolean anyProgress = false; + boolean allComplete = !BoM.tree.goal.children.isEmpty(); + + for (MaterialNode child : BoM.tree.goal.children) { + if (child.progress != ProgressState.UNSTARTED) { + anyProgress = true; + } + if (child.progress != ProgressState.COMPLETED) { + allComplete = false; + } + } + + if (allComplete) { + BoM.tree.goal.progress = ProgressState.COMPLETED; + } else if (anyProgress) { + BoM.tree.goal.progress = ProgressState.PARTIAL; + } else { + BoM.tree.goal.progress = ProgressState.UNSTARTED; + } + } +} diff --git a/src/main/java/net/liukrast/schematicdisplay/mixin/ClipboardScreenMixin.java b/src/main/java/net/liukrast/schematicdisplay/mixin/ClipboardScreenMixin.java index 08d60d7..add8f6e 100644 --- a/src/main/java/net/liukrast/schematicdisplay/mixin/ClipboardScreenMixin.java +++ b/src/main/java/net/liukrast/schematicdisplay/mixin/ClipboardScreenMixin.java @@ -33,8 +33,8 @@ protected ClipboardScreenMixin(final Component ignored) { private void init(CallbackInfo ci) { final int x = guiLeft; final int y = guiTop - 8; - final IconButton customButton = new IconButton(x + 234, y + 197, AllIcons.I_WHITELIST).withCallback(() -> ClipboardScreenUtils.load(pages)); + final IconButton customButton = new IconButton(x + 234, y + 197, AllIcons.I_SCHEMATIC).withCallback(() -> ClipboardScreenUtils.load(pages)); customButton.setToolTip(emi_create_schematics$TOOLTIP); this.addRenderableWidget(customButton); } -} \ No newline at end of file +} diff --git a/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java b/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java new file mode 100644 index 0000000..abeb66a --- /dev/null +++ b/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java @@ -0,0 +1,22 @@ +package net.liukrast.schematicdisplay.mixin; + +import dev.emi.emi.api.recipe.EmiPlayerInventory; +import dev.emi.emi.bom.BoM; +import dev.emi.emi.runtime.EmiFavorites; +import net.liukrast.schematicdisplay.clipboard.ClipboardTreeRecipe; +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.CallbackInfo; + +@Mixin(EmiFavorites.class) +public abstract class EmiFavoritesMixin { + @Inject(method = "updateSynthetic", at = @At("TAIL"), remap = false) + private static void emi_create_schematics$hideVirtualRootRecipe(EmiPlayerInventory inv, CallbackInfo ci) { + if (BoM.tree == null || !ClipboardTreeRecipe.isVirtual(BoM.tree.goal.recipe)) { + return; + } + + EmiFavorites.syntheticFavorites.removeIf(favorite -> ClipboardTreeRecipe.isVirtual(favorite.getRecipe())); + } +} diff --git a/src/main/resources/assets/emi_create_schematics/lang/en_us.json b/src/main/resources/assets/emi_create_schematics/lang/en_us.json index b72345c..4deed8c 100644 --- a/src/main/resources/assets/emi_create_schematics/lang/en_us.json +++ b/src/main/resources/assets/emi_create_schematics/lang/en_us.json @@ -1,3 +1,7 @@ { - "gui.emi_create_schematics.clipboard.favourite": "Transfer all items to EMI Favourites" -} \ No newline at end of file + "gui.emi_create_schematics.clipboard.favourite": "Open the EMI recipe tree from this clipboard", + "gui.emi_create_schematics.clipboard.tree_empty": "No unchecked item requirements were found on this clipboard", + "gui.emi_create_schematics.clipboard.tree_output": "Schematic Materials", + "gui.emi_create_schematics.clipboard.tree_recipe": "Clipboard Checklist", + "gui.emi_create_schematics.clipboard.tree_more": "+%s more" +} diff --git a/src/main/resources/assets/emi_create_schematics/lang/it_it.json b/src/main/resources/assets/emi_create_schematics/lang/it_it.json index 2b931fb..c792612 100644 --- a/src/main/resources/assets/emi_create_schematics/lang/it_it.json +++ b/src/main/resources/assets/emi_create_schematics/lang/it_it.json @@ -1,3 +1,7 @@ { - "gui.emi_create_schematics.clipboard.favourite": "Trasferisci tutti gli item nei preferiti di EMI" -} \ No newline at end of file + "gui.emi_create_schematics.clipboard.favourite": "Apri l'albero ricette di EMI da questa clipboard", + "gui.emi_create_schematics.clipboard.tree_empty": "Nessun requisito oggetto non spuntato trovato su questa clipboard", + "gui.emi_create_schematics.clipboard.tree_output": "Materiali dello schema", + "gui.emi_create_schematics.clipboard.tree_recipe": "Checklist della clipboard", + "gui.emi_create_schematics.clipboard.tree_more": "+%s altri" +} diff --git a/src/main/resources/emi_create_schematics.mixins.json b/src/main/resources/emi_create_schematics.mixins.json index e4196f5..91e9859 100644 --- a/src/main/resources/emi_create_schematics.mixins.json +++ b/src/main/resources/emi_create_schematics.mixins.json @@ -8,7 +8,9 @@ "ItemEmiStackMixin" ], "client": [ - "ClipboardScreenMixin" + "BoMScreenMixin", + "ClipboardScreenMixin", + "EmiFavoritesMixin" ], "injectors": { "defaultRequire": 1 From 6a81cc0d21876affdb0142d3bcc53572d2bae8e0 Mon Sep 17 00:00:00 2001 From: brokestar233 <3765589194@qq.com> Date: Tue, 7 Jul 2026 23:14:43 +0800 Subject: [PATCH 2/2] Improved virtual root completion in EMI recipe tree Signed-off-by: brokestar233 <3765589194@qq.com> --- .../clipboard/ClipboardTreeRecipe.java | 13 +++++++++ .../mixin/BoMScreenMixin.java | 29 ++++++++++++++++++- .../mixin/EmiFavoritesMixin.java | 19 ++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java b/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java index 42a5700..4f0729e 100644 --- a/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java +++ b/src/main/java/net/liukrast/schematicdisplay/clipboard/ClipboardTreeRecipe.java @@ -15,6 +15,7 @@ public final class ClipboardTreeRecipe extends BasicEmiRecipe { private static final int DISPLAYED_INPUTS = 9; private final Component overflowLabel; + private boolean completed; public ClipboardTreeRecipe(List inputs, EmiStack output, ResourceLocation id) { super(VanillaEmiRecipeCategories.CRAFTING, id, 118, 72); @@ -31,6 +32,18 @@ public static boolean isVirtual(EmiRecipe recipe) { return recipe instanceof ClipboardTreeRecipe; } + public static ClipboardTreeRecipe getVirtual(EmiRecipe recipe) { + return recipe instanceof ClipboardTreeRecipe virtual ? virtual : null; + } + + public boolean isCompleted() { + return completed; + } + + public void markCompleted() { + completed = true; + } + @Override public boolean hideCraftable() { return true; diff --git a/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java b/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java index 9770fbe..8a3f954 100644 --- a/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java +++ b/src/main/java/net/liukrast/schematicdisplay/mixin/BoMScreenMixin.java @@ -5,6 +5,7 @@ import dev.emi.emi.bom.ProgressState; import dev.emi.emi.screen.BoMScreen; import net.liukrast.schematicdisplay.clipboard.ClipboardTreeRecipe; +import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -14,7 +15,23 @@ public abstract class BoMScreenMixin { @Inject(method = "recalculateTree", at = @At("TAIL"), remap = false) private void emi_create_schematics$markVirtualRootProgress(CallbackInfo ci) { - if (BoM.tree == null || !ClipboardTreeRecipe.isVirtual(BoM.tree.goal.recipe) || BoM.tree.goal.children == null) { + if (BoM.tree == null) { + return; + } + + ClipboardTreeRecipe virtualRecipe = ClipboardTreeRecipe.getVirtual(BoM.tree.goal.recipe); + if (virtualRecipe == null || BoM.tree.goal.children == null) { + return; + } + + if (!virtualRecipe.isCompleted() && Minecraft.getInstance().player != null + && dev.emi.emi.api.recipe.EmiPlayerInventory.of(Minecraft.getInstance().player).canCraft(virtualRecipe)) { + virtualRecipe.markCompleted(); + BoM.craftingMode = false; + } + + if (virtualRecipe.isCompleted()) { + emi_create_schematics$completeNode(BoM.tree.goal); return; } @@ -38,4 +55,14 @@ public abstract class BoMScreenMixin { BoM.tree.goal.progress = ProgressState.UNSTARTED; } } + + private void emi_create_schematics$completeNode(MaterialNode node) { + node.progress = ProgressState.COMPLETED; + if (node.children == null) { + return; + } + for (MaterialNode child : node.children) { + emi_create_schematics$completeNode(child); + } + } } diff --git a/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java b/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java index abeb66a..c1075dc 100644 --- a/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java +++ b/src/main/java/net/liukrast/schematicdisplay/mixin/EmiFavoritesMixin.java @@ -13,10 +13,25 @@ public abstract class EmiFavoritesMixin { @Inject(method = "updateSynthetic", at = @At("TAIL"), remap = false) private static void emi_create_schematics$hideVirtualRootRecipe(EmiPlayerInventory inv, CallbackInfo ci) { - if (BoM.tree == null || !ClipboardTreeRecipe.isVirtual(BoM.tree.goal.recipe)) { + if (BoM.tree == null) { return; } - EmiFavorites.syntheticFavorites.removeIf(favorite -> ClipboardTreeRecipe.isVirtual(favorite.getRecipe())); + ClipboardTreeRecipe virtualRecipe = ClipboardTreeRecipe.getVirtual(BoM.tree.goal.recipe); + if (virtualRecipe == null) { + return; + } + + if (!virtualRecipe.isCompleted() && inv.canCraft(virtualRecipe)) { + virtualRecipe.markCompleted(); + BoM.craftingMode = false; + } + + if (virtualRecipe.isCompleted()) { + EmiFavorites.syntheticFavorites.clear(); + return; + } + + EmiFavorites.syntheticFavorites.removeIf(favorite -> favorite.getRecipe() == virtualRecipe); } }