diff --git a/xplat/src/main/java/dev/emi/emi/EmiRenderHelper.java b/xplat/src/main/java/dev/emi/emi/EmiRenderHelper.java index 9f1e5b18..2a3a1a54 100644 --- a/xplat/src/main/java/dev/emi/emi/EmiRenderHelper.java +++ b/xplat/src/main/java/dev/emi/emi/EmiRenderHelper.java @@ -264,6 +264,13 @@ public static void renderAmount(EmiDrawContext context, int x, int y, Text amoun context.pop(); } + public static void renderText(EmiDrawContext context, int x, int y, Text text) { + context.push(); + context.matrices().translate(0, 0, 200); + context.drawTextWithShadow(text, x + 1, y + 5, -1); + context.pop(); + } + public static void renderIngredient(EmiIngredient ingredient, EmiDrawContext context, int x, int y) { RenderSystem.enableDepthTest(); context.push(); diff --git a/xplat/src/main/java/dev/emi/emi/VanillaPlugin.java b/xplat/src/main/java/dev/emi/emi/VanillaPlugin.java index 6d396a56..9b882706 100644 --- a/xplat/src/main/java/dev/emi/emi/VanillaPlugin.java +++ b/xplat/src/main/java/dev/emi/emi/VanillaPlugin.java @@ -41,14 +41,7 @@ import dev.emi.emi.api.recipe.EmiWorldInteractionRecipe; import dev.emi.emi.api.render.EmiRenderable; import dev.emi.emi.api.render.EmiTexture; -import dev.emi.emi.api.stack.Comparison; -import dev.emi.emi.api.stack.EmiIngredient; -import dev.emi.emi.api.stack.EmiRegistryAdapter; -import dev.emi.emi.api.stack.EmiStack; -import dev.emi.emi.api.stack.FluidEmiStack; -import dev.emi.emi.api.stack.ItemEmiStack; -import dev.emi.emi.api.stack.ListEmiIngredient; -import dev.emi.emi.api.stack.TagEmiIngredient; +import dev.emi.emi.api.stack.*; import dev.emi.emi.api.widget.Bounds; import dev.emi.emi.api.widget.GeneratedSlotWidget; import dev.emi.emi.config.EffectLocation; @@ -95,10 +88,7 @@ import dev.emi.emi.runtime.EmiDrawContext; import dev.emi.emi.runtime.EmiLog; import dev.emi.emi.runtime.EmiReloadLog; -import dev.emi.emi.stack.serializer.FluidEmiStackSerializer; -import dev.emi.emi.stack.serializer.ItemEmiStackSerializer; -import dev.emi.emi.stack.serializer.ListEmiIngredientSerializer; -import dev.emi.emi.stack.serializer.TagEmiIngredientSerializer; +import dev.emi.emi.stack.serializer.*; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; @@ -217,6 +207,7 @@ public void initialize(EmiInitRegistry registry) { registry.addIngredientSerializer(FluidEmiStack.class, new FluidEmiStackSerializer()); registry.addIngredientSerializer(TagEmiIngredient.class, new TagEmiIngredientSerializer()); registry.addIngredientSerializer(ListEmiIngredient.class, new ListEmiIngredientSerializer()); + registry.addIngredientSerializer(SearchEmiIngredient.class, new SearchEmiIngredientSerializer()); registry.addRegistryAdapter(EmiRegistryAdapter.simple(Item.class, EmiPort.getItemRegistry(), EmiStack::of)); registry.addRegistryAdapter(EmiRegistryAdapter.simple(Fluid.class, EmiPort.getFluidRegistry(), EmiStack::of)); diff --git a/xplat/src/main/java/dev/emi/emi/api/stack/EmiIngredient.java b/xplat/src/main/java/dev/emi/emi/api/stack/EmiIngredient.java index ba4a58bb..dcdacb6f 100644 --- a/xplat/src/main/java/dev/emi/emi/api/stack/EmiIngredient.java +++ b/xplat/src/main/java/dev/emi/emi/api/stack/EmiIngredient.java @@ -140,4 +140,8 @@ public static EmiIngredient of(List list, long amount) return EmiTags.getIngredient(tagType, list.stream().flatMap(i -> i.getEmiStacks().stream()).toList(), amount); } } + + public static EmiIngredient of(String text, List results) { + return new SearchEmiIngredient(text, results); + } } diff --git a/xplat/src/main/java/dev/emi/emi/api/stack/SearchEmiIngredient.java b/xplat/src/main/java/dev/emi/emi/api/stack/SearchEmiIngredient.java new file mode 100644 index 00000000..071bdae0 --- /dev/null +++ b/xplat/src/main/java/dev/emi/emi/api/stack/SearchEmiIngredient.java @@ -0,0 +1,106 @@ +package dev.emi.emi.api.stack; + +import com.google.common.collect.Lists; +import dev.emi.emi.EmiPort; +import dev.emi.emi.api.render.EmiRender; +import dev.emi.emi.screen.tooltip.EmiTextTooltipWrapper; +import dev.emi.emi.screen.tooltip.IngredientTooltipComponent; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +public class SearchEmiIngredient implements EmiIngredient { + private final List results; + private final List fullResults; + + public final String content; + + public SearchEmiIngredient(String content, List results) { + this.results = results; + this.fullResults = results.stream().flatMap(i -> i.getEmiStacks().stream()).toList(); + if (fullResults.isEmpty()) { + throw new IllegalArgumentException("SearchEmiIngredient cannot be and empty search"); + } + + this.content = content; + } + + @Override + public void render(DrawContext draw, int x, int y, float delta, int flags) { + int item = (int) (System.currentTimeMillis() / 1000 % results.size()); + EmiIngredient current = results.get(item); + if ((flags & RENDER_ICON) != 0) { + current.render(draw, x, y, delta, -1 ^ RENDER_AMOUNT); + } + if ((flags & RENDER_INGREDIENT) != 0) { + EmiRender.renderIngredientIcon(this, draw, x, y); + } + +// // Maybe render a couple of letters of the search above the icon +// EmiDrawContext context = EmiDrawContext.wrap(draw); +// EmiRenderHelper.renderText(context, x, y, EmiPort.literal(content.substring(0, 3))); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof SearchEmiIngredient ingredient && ingredient.content.equals(this.content); + } + + @Override + public EmiIngredient copy() { + return new SearchEmiIngredient(content, results); + } + + @Override + public long getAmount() { + return 1; + } + + @Override + public EmiIngredient setAmount(long amount) { + return null; + } + + @Override + public float getChance() { + return 1; + } + + @Override + public EmiIngredient setChance(float chance) { + return null; + } + + @Override + public List getEmiStacks() { + return EmiStack.EMPTY.getEmiStacks(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public List getTooltip() { + List tooltip = Lists.newArrayList(); + tooltip.add(new EmiTextTooltipWrapper(this, EmiPort.ordered(EmiPort.literal(content)))); + tooltip.add(new IngredientTooltipComponent(results)); + int item = (int) (System.currentTimeMillis() / 1000 % results.size()); + tooltip.addAll(results.get(item).copy().setAmount(1).getTooltip()); + return tooltip; + } + + @ApiStatus.Internal + public String getContent() { + return content; + } + + @ApiStatus.Internal + public List getResults() { + return results; + } +} diff --git a/xplat/src/main/java/dev/emi/emi/bom/BoM.java b/xplat/src/main/java/dev/emi/emi/bom/BoM.java index 7c000791..0fca2cdb 100644 --- a/xplat/src/main/java/dev/emi/emi/bom/BoM.java +++ b/xplat/src/main/java/dev/emi/emi/bom/BoM.java @@ -1,8 +1,10 @@ package dev.emi.emi.bom; +import java.util.List; import java.util.Map; import java.util.Set; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.JsonArray; @@ -12,6 +14,7 @@ import dev.emi.emi.EmiPort; import dev.emi.emi.api.EmiApi; +import dev.emi.emi.api.recipe.EmiPlayerInventory; import dev.emi.emi.api.recipe.EmiRecipe; import dev.emi.emi.api.recipe.EmiResolutionRecipe; import dev.emi.emi.api.stack.EmiIngredient; @@ -26,7 +29,10 @@ public class BoM { private static RecipeDefaults defaults = new RecipeDefaults(); - public static MaterialTree tree; + public static List trees = Lists.newArrayList(); + public static int treeIndex = -1; + public static TreeCost combinedCost = new TreeCost(); + public static TreeCost combinedProgress = new TreeCost(); public static Map defaultRecipes = Maps.newHashMap(); public static Map addedRecipes = Maps.newHashMap(); public static Set disabledRecipes = Sets.newHashSet(); @@ -166,12 +172,70 @@ public static EmiRecipe getRecipe(EmiIngredient stack) { } public static void setGoal(EmiRecipe recipe) { - tree = new MaterialTree(recipe); + trees.clear(); + trees.add(new MaterialTree(recipe)); + treeIndex = 0; craftingMode = false; + recalculate(); + } + + public static void addGoal(EmiRecipe recipe) { + if (trees.isEmpty()) { + setGoal(recipe); + return; + } + trees.add(new MaterialTree(recipe)); + treeIndex = trees.size() - 1; + recalculate(); + } + + public static MaterialTree getTree() { + if (treeIndex < 0 || treeIndex >= trees.size()) { + return null; + } + return trees.get(treeIndex); + } + + public static List getTrees() { + return trees; + } + + public static void selectTree(int index) { + if (trees.isEmpty()) { + treeIndex = -1; + return; + } + treeIndex = Math.max(0, Math.min(index, trees.size() - 1)); + } + + public static void cycleTree(int delta) { + if (trees.isEmpty()) { + treeIndex = -1; + return; + } + int size = trees.size(); + treeIndex = ((treeIndex + delta) % size + size) % size; + } + + public static void removeTree(int index) { + if (index < 0 || index >= trees.size()) { + return; + } + trees.remove(index); + if (trees.isEmpty()) { + treeIndex = -1; + craftingMode = false; + } else { + treeIndex = Math.max(0, Math.min(treeIndex, trees.size() - 1)); + } + recalculate(); } public static void addResolution(EmiIngredient ingredient, EmiRecipe recipe) { - tree.addResolution(ingredient, recipe); + MaterialTree tree = getTree(); + if (tree != null) { + tree.addResolution(ingredient, recipe); + } } public static boolean isDefaultRecipe(EmiIngredient stack, EmiRecipe recipe) { @@ -227,12 +291,25 @@ public static void removeRecipe(EmiIngredient stack, EmiRecipe recipe) { recalculate(); } - private static void recalculate() { - if (tree != null) { - tree.recalculate(); + public static void calculateCombinedCosts(EmiPlayerInventory inventory) { + combinedProgress.clear(); + for (MaterialTree tree : trees) { + tree.calculateProgress(inventory); + combinedProgress.merge(tree.cost); + } + combinedCost.clear(); + for (MaterialTree tree : trees) { + tree.calculateCost(); + combinedCost.merge(tree.cost); } } + private static void recalculate() { + for (MaterialTree tree : trees) { + tree.recalculate(); + } + } + public static enum DefaultStatus { EMPTY, PARTIAL, diff --git a/xplat/src/main/java/dev/emi/emi/bom/MaterialTree.java b/xplat/src/main/java/dev/emi/emi/bom/MaterialTree.java index a5e1e8aa..233b8750 100644 --- a/xplat/src/main/java/dev/emi/emi/bom/MaterialTree.java +++ b/xplat/src/main/java/dev/emi/emi/bom/MaterialTree.java @@ -22,6 +22,11 @@ public MaterialTree(EmiRecipe recipe) { recalculate(); } + public MaterialTree(MaterialNode goal, long batches) { + this.goal = goal; + this.batches = Math.max(1, batches); + } + public EmiRecipe getRecipe(EmiIngredient stack) { EmiRecipe recipe = resolutions.get(stack); if (recipe == null && !resolutions.containsKey(stack)) { diff --git a/xplat/src/main/java/dev/emi/emi/bom/TreeCost.java b/xplat/src/main/java/dev/emi/emi/bom/TreeCost.java index be0c9017..9e29b8fa 100644 --- a/xplat/src/main/java/dev/emi/emi/bom/TreeCost.java +++ b/xplat/src/main/java/dev/emi/emi/bom/TreeCost.java @@ -17,19 +17,61 @@ public class TreeCost { public Map remainders = Maps.newHashMap(); public Map chanceRemainders = Maps.newHashMap(); - public void calculate(MaterialNode node, long batches) { + public void clear() { costs.clear(); chanceCosts.clear(); remainders.clear(); chanceRemainders.clear(); + } + + public void merge(TreeCost other) { + for (FlatMaterialCost cost : other.costs.values()) { + FlatMaterialCost existing = costs.get(cost.ingredient); + if (existing == null) { + costs.put(cost.ingredient, new FlatMaterialCost(cost.ingredient, cost.amount)); + } else { + existing.amount += cost.amount; + } + } + for (ChanceMaterialCost cost : other.chanceCosts.values()) { + ChanceMaterialCost existing = chanceCosts.get(cost.ingredient); + if (existing == null) { + existing = new ChanceMaterialCost(cost.ingredient, cost.amount, cost.chance); + chanceCosts.put(cost.ingredient, existing); + } else { + existing.merge(cost.amount, cost.chance); + } + existing.minBatch(cost.minBatch); + } + for (FlatMaterialCost remainder : other.remainders.values()) { + EmiStack key = (EmiStack) remainder.ingredient; + FlatMaterialCost existing = remainders.get(key); + if (existing == null) { + remainders.put(key, new FlatMaterialCost(key, remainder.amount)); + } else { + existing.amount += remainder.amount; + } + } + for (ChanceMaterialCost remainder : other.chanceRemainders.values()) { + EmiStack key = (EmiStack) remainder.ingredient; + ChanceMaterialCost existing = chanceRemainders.get(key); + if (existing == null) { + existing = new ChanceMaterialCost(key, remainder.amount, remainder.chance); + chanceRemainders.put(key, existing); + } else { + existing.merge(remainder.amount, remainder.chance); + } + existing.minBatch(remainder.minBatch); + } + } + + public void calculate(MaterialNode node, long batches) { + clear(); calculateCost(node, batches * node.amount, ChanceState.DEFAULT, false); } public void calculateProgress(MaterialNode node, long batches, EmiPlayerInventory inventory) { - costs.clear(); - chanceCosts.clear(); - remainders.clear(); - chanceRemainders.clear(); + clear(); for (EmiStack stack : inventory.inventory.values()) { stack = stack.copy(); remainders.put(stack, new FlatMaterialCost(stack, stack.getAmount())); diff --git a/xplat/src/main/java/dev/emi/emi/config/EmiConfig.java b/xplat/src/main/java/dev/emi/emi/config/EmiConfig.java index 3ea8cf26..e2731a1c 100644 --- a/xplat/src/main/java/dev/emi/emi/config/EmiConfig.java +++ b/xplat/src/main/java/dev/emi/emi/config/EmiConfig.java @@ -241,7 +241,8 @@ public class EmiConfig { @ConfigValue("ui.right-sidebar-pages") public static SidebarPages rightSidebarPages = new SidebarPages(List.of( new SidebarPages.SidebarPage(SidebarType.INDEX), - new SidebarPages.SidebarPage(SidebarType.CRAFTABLES) + new SidebarPages.SidebarPage(SidebarType.CRAFTABLES), + new SidebarPages.SidebarPage(SidebarType.TREE_BOOKMARKS) ), SidebarSettings.RIGHT); @Comment("The subpanels in the right sidebar") @@ -363,6 +364,19 @@ public class EmiConfig { @ConfigValue("binds.clear-search") public static EmiBind clearSearch = new EmiBind("key.emi.clear_search", InputUtil.UNKNOWN_KEY.getCode()); + @Comment("Add current search as a Bookmark") + @ConfigValue("binds.add-bookmark") + public static EmiBind addBookmark = new EmiBind("key.emi.add_bookmark", EmiInput.CONTROL_MASK, GLFW.GLFW_KEY_ENTER); + + @Comment("Add current recipe tree as a Tree Bookmark") + @ConfigValue("binds.add-tree-bookmark") + public static EmiBind addTreeBookmark = new EmiBind("key.emi.add_tree_bookmark", EmiInput.CONTROL_MASK, GLFW.GLFW_KEY_ENTER); + + @Comment("Rename an existing Tree Bookmark in the sidebar") + @ConfigValue("binds.rename-tree-bookmark") + public static EmiBind renameTreeBookmark = new EmiBind("key.emi.rename_tree_bookmark", + new EmiBind.ModifiedKey(InputUtil.Type.MOUSE.createFromCode(1), EmiInput.SHIFT_MASK)); + @Comment("Display the recipes for creating a stack.") @ConfigValue("binds.view-recipes") public static EmiBind viewRecipes = new EmiBind("key.emi.view_recipes", diff --git a/xplat/src/main/java/dev/emi/emi/config/SidebarType.java b/xplat/src/main/java/dev/emi/emi/config/SidebarType.java index 9ccbdb48..fab86eee 100644 --- a/xplat/src/main/java/dev/emi/emi/config/SidebarType.java +++ b/xplat/src/main/java/dev/emi/emi/config/SidebarType.java @@ -13,6 +13,8 @@ public enum SidebarType implements ConfigEnum { CRAFT_HISTORY("craft-history", 64, 146), EMPTY("empty", 96, 146), CHESS("chess", 48, 146), + BOOKMARKS("bookmarks", 112, 146), + TREE_BOOKMARKS("tree_bookmarks", 128, 146), ; private final String name; diff --git a/xplat/src/main/java/dev/emi/emi/network/CommandS2CPacket.java b/xplat/src/main/java/dev/emi/emi/network/CommandS2CPacket.java index f27253f7..85085c0d 100644 --- a/xplat/src/main/java/dev/emi/emi/network/CommandS2CPacket.java +++ b/xplat/src/main/java/dev/emi/emi/network/CommandS2CPacket.java @@ -52,9 +52,9 @@ public void apply(PlayerEntity player) { } } else if (type == EmiCommands.TREE_RESOLUTION) { EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id); - if (recipe != null && BoM.tree != null) { + if (recipe != null && BoM.getTree() != null) { for (EmiStack stack : recipe.getOutputs()) { - BoM.tree.addResolution(stack, recipe); + BoM.addResolution(stack, recipe); } } } diff --git a/xplat/src/main/java/dev/emi/emi/runtime/EmiBookmarks.java b/xplat/src/main/java/dev/emi/emi/runtime/EmiBookmarks.java new file mode 100644 index 00000000..98fbdf6e --- /dev/null +++ b/xplat/src/main/java/dev/emi/emi/runtime/EmiBookmarks.java @@ -0,0 +1,48 @@ +package dev.emi.emi.runtime; + +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.api.stack.SearchEmiIngredient; +import dev.emi.emi.api.stack.serializer.EmiIngredientSerializer; + +import java.util.List; + +public class EmiBookmarks { + public static List bookmarks = Lists.newArrayList(); + + public static JsonArray save() { + JsonArray arr = new JsonArray(); + for (SearchEmiIngredient bookmark : bookmarks) { + JsonElement serialized = EmiIngredientSerializer.getSerialized(bookmark); + arr.add(serialized); + } + return arr; + } + + public static void load(JsonArray arr) { + bookmarks.clear(); + for (JsonElement element : arr) { + EmiIngredient bookmark = EmiIngredientSerializer.getDeserialized(element); + + if (bookmark instanceof SearchEmiIngredient) { + bookmarks.add((SearchEmiIngredient) bookmark); + } + } + } + + public static void addBookmark(String content, List items) { + SearchEmiIngredient bookmark = new SearchEmiIngredient(content, items); + if (!bookmarks.contains(bookmark)) { + bookmarks.add(bookmark); + } + EmiPersistentData.save(); + } + + public static void removeBookmark(SearchEmiIngredient bookmark) { + bookmarks.remove(bookmark); + EmiPersistentData.save(); + } +} + diff --git a/xplat/src/main/java/dev/emi/emi/runtime/EmiFavorites.java b/xplat/src/main/java/dev/emi/emi/runtime/EmiFavorites.java index 21a346ae..a98c61b0 100644 --- a/xplat/src/main/java/dev/emi/emi/runtime/EmiFavorites.java +++ b/xplat/src/main/java/dev/emi/emi/runtime/EmiFavorites.java @@ -22,7 +22,9 @@ import dev.emi.emi.bom.BoM; import dev.emi.emi.bom.ChanceMaterialCost; import dev.emi.emi.bom.FlatMaterialCost; +import dev.emi.emi.bom.MaterialTree; import dev.emi.emi.bom.MaterialNode; +import dev.emi.emi.bom.TreeCost; import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2LongMap; import net.minecraft.util.Identifier; @@ -191,20 +193,32 @@ public static void addFavorite(EmiIngredient stack, EmiRecipe context) { public static void updateSynthetic(EmiPlayerInventory inv) { syntheticFavorites.clear(); - if (BoM.tree != null && BoM.craftingMode) { - BoM.tree.calculateCost(); - Map originalCosts = Maps.newHashMap(BoM.tree.cost.costs); - Map chancedCosts = Maps.newHashMap(BoM.tree.cost.chanceCosts); + List trees = BoM.getTrees(); + if (!trees.isEmpty() && BoM.craftingMode) { + TreeCost originalCost = new TreeCost(); + for (MaterialTree tree : trees) { + tree.calculateCost(); + originalCost.merge(tree.cost); + } + Map originalCosts = Maps.newHashMap(originalCost.costs); + Map chancedCosts = Maps.newHashMap(originalCost.chanceCosts); Object2LongMap originalBatches = new Object2LongLinkedOpenHashMap<>(); Object2LongMap originalAmounts = new Object2LongLinkedOpenHashMap<>(); EmiPlayerInventory emptyInventory = new EmiPlayerInventory(List.of()); emptyInventory.inventory.clear(); - BoM.tree.calculateProgress(emptyInventory); - countRecipes(originalBatches, originalAmounts, BoM.tree.goal); - BoM.tree.calculateProgress(inv); + for (MaterialTree tree : trees) { + tree.calculateProgress(emptyInventory); + countRecipes(originalBatches, originalAmounts, tree.goal); + } + TreeCost remainingCost = new TreeCost(); Object2LongMap batches = new Object2LongLinkedOpenHashMap<>(); Object2LongMap amounts = new Object2LongLinkedOpenHashMap<>(); - countRecipes(batches, amounts, BoM.tree.goal); + for (MaterialTree tree : trees) { + tree.calculateProgress(inv); + countRecipes(batches, amounts, tree.goal); + remainingCost.merge(tree.cost); + } + BoM.calculateCombinedCosts(inv); boolean hasSomething = false; for (Object2LongMap.Entry entry : batches.object2LongEntrySet()) { EmiRecipe recipe = entry.getKey(); @@ -225,12 +239,12 @@ public static void updateSynthetic(EmiPlayerInventory inv) { if (!hasSomething) { BoM.craftingMode = false; } else { - for (FlatMaterialCost cost : BoM.tree.cost.costs.values()) { + for (FlatMaterialCost cost : remainingCost.costs.values()) { if (cost.amount > 0) { syntheticFavorites.add(new EmiFavorite.Synthetic(cost.ingredient, cost.amount, originalCosts.getOrDefault(cost.ingredient, cost).amount)); } } - for (ChanceMaterialCost cost : BoM.tree.cost.chanceCosts.values()) { + for (ChanceMaterialCost cost : remainingCost.chanceCosts.values()) { if (cost.getEffectiveAmount() > 0) { long needed = cost.getEffectiveAmount(); if (chancedCosts.containsKey(cost.ingredient)) { diff --git a/xplat/src/main/java/dev/emi/emi/runtime/EmiPersistentData.java b/xplat/src/main/java/dev/emi/emi/runtime/EmiPersistentData.java index 03d36027..f7531c41 100644 --- a/xplat/src/main/java/dev/emi/emi/runtime/EmiPersistentData.java +++ b/xplat/src/main/java/dev/emi/emi/runtime/EmiPersistentData.java @@ -5,6 +5,7 @@ import java.io.FileWriter; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import dev.emi.emi.bom.BoM; @@ -18,6 +19,8 @@ public static void save() { try { JsonObject json = new JsonObject(); json.add("favorites", EmiFavorites.save()); + json.add("bookmarks", EmiBookmarks.save()); + json.add("tree_bookmarks", EmiTreeBookmarks.save()); EmiSidebars.save(json); json.add("recipe_defaults", BoM.saveAdded()); json.add("hidden_stacks", EmiHidden.save()); @@ -38,6 +41,12 @@ public static void load() { if (JsonHelper.hasArray(json, "favorites")) { EmiFavorites.load(JsonHelper.getArray(json, "favorites")); } + if (JsonHelper.hasArray(json, "bookmarks")) { + EmiBookmarks.load(JsonHelper.getArray(json, "bookmarks")); + } + if (JsonHelper.hasArray(json, "tree_bookmarks")) { + EmiTreeBookmarks.load(JsonHelper.getArray(json, "tree_bookmarks")); + } EmiSidebars.load(json); if (JsonHelper.hasJsonObject(json, "recipe_defaults")) { BoM.loadAdded(JsonHelper.getObject(json, "recipe_defaults")); diff --git a/xplat/src/main/java/dev/emi/emi/runtime/EmiSidebars.java b/xplat/src/main/java/dev/emi/emi/runtime/EmiSidebars.java index 260622b6..08e481e6 100644 --- a/xplat/src/main/java/dev/emi/emi/runtime/EmiSidebars.java +++ b/xplat/src/main/java/dev/emi/emi/runtime/EmiSidebars.java @@ -30,6 +30,8 @@ public static List getStacks(SidebarType type) { case INDEX -> EmiConfig.editMode ? EmiStackList.stacks : EmiStackList.filteredStacks; case CRAFTABLES -> craftables; case FAVORITES -> EmiFavorites.favoriteSidebar; + case BOOKMARKS -> EmiBookmarks.bookmarks; + case TREE_BOOKMARKS -> EmiTreeBookmarks.bookmarks; case LOOKUP_HISTORY -> lookupHistory; case CRAFT_HISTORY -> craftHistory; case EMPTY -> List.of(); diff --git a/xplat/src/main/java/dev/emi/emi/runtime/EmiTreeBookmarks.java b/xplat/src/main/java/dev/emi/emi/runtime/EmiTreeBookmarks.java new file mode 100644 index 00000000..3bf8a7b8 --- /dev/null +++ b/xplat/src/main/java/dev/emi/emi/runtime/EmiTreeBookmarks.java @@ -0,0 +1,701 @@ +package dev.emi.emi.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Lists; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import dev.emi.emi.EmiPort; +import dev.emi.emi.EmiRenderHelper; +import dev.emi.emi.api.EmiApi; +import dev.emi.emi.api.recipe.EmiResolutionRecipe; +import dev.emi.emi.api.recipe.EmiRecipe; +import dev.emi.emi.api.recipe.EmiPlayerInventory; +import dev.emi.emi.api.render.EmiRender; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.api.stack.serializer.EmiIngredientSerializer; +import dev.emi.emi.bom.BoM; +import dev.emi.emi.bom.FoldState; +import dev.emi.emi.bom.MaterialNode; +import dev.emi.emi.bom.MaterialTree; +import dev.emi.emi.bom.ProgressState; +import dev.emi.emi.bom.TreeCost; +import dev.emi.emi.runtime.EmiDrawContext; +import dev.emi.emi.screen.BoMScreen; +import dev.emi.emi.screen.tooltip.EmiTextTooltipWrapper; +import dev.emi.emi.screen.tooltip.IngredientTooltipComponent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; + +public class EmiTreeBookmarks { + public static List bookmarks = Lists.newArrayList(); + + public static JsonArray save() { + JsonArray arr = new JsonArray(); + for (TreeBookmark bookmark : bookmarks) { + arr.add(bookmark.serialize()); + } + return arr; + } + + public static void load(JsonArray arr) { + bookmarks.clear(); + for (JsonElement element : arr) { + if (element.isJsonObject()) { + TreeBookmark bookmark = TreeBookmark.deserialize(element.getAsJsonObject()); + if (bookmark != null) { + bookmarks.add(bookmark); + } + } + } + } + + public static void addBookmark(List trees, int selectedIndex, boolean craftingMode) { + addBookmark(trees, selectedIndex, craftingMode, null); + } + + public static void addBookmark(List trees, int selectedIndex, boolean craftingMode, String name) { + if (trees == null || trees.isEmpty()) { + return; + } + TreeBookmark bookmark = TreeBookmark.fromTrees(trees, selectedIndex, craftingMode, name); + if (bookmark != null) { + bookmarks.add(bookmark); + EmiPersistentData.save(); + } + } + + public static String suggestName(List trees, int selectedIndex, boolean craftingMode) { + TreeBookmark bookmark = TreeBookmark.fromTrees(trees, selectedIndex, craftingMode, null); + if (bookmark == null) { + return EmiPort.translatable("emi.tree_bookmark").getString(); + } + return bookmark.getName(); + } + + public static void removeBookmark(TreeBookmark bookmark) { + bookmarks.remove(bookmark); + EmiPersistentData.save(); + } + + public static void renameBookmark(TreeBookmark bookmark, String name) { + int index = bookmarks.indexOf(bookmark); + if (index < 0) { + return; + } + TreeBookmark renamed = bookmark.withName(name); + bookmarks.set(index, renamed); + EmiPersistentData.save(); + } + + public static void apply(TreeBookmark bookmark) { + if (bookmark == null) { + return; + } + List trees = bookmark.instantiateTrees(); + BoM.trees.clear(); + BoM.trees.addAll(trees); + if (trees.isEmpty()) { + BoM.treeIndex = -1; + BoM.craftingMode = false; + } else { + BoM.treeIndex = Math.max(0, Math.min(bookmark.getSelectedIndex(), trees.size() - 1)); + BoM.craftingMode = bookmark.isCraftingMode(); + } + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null) { + BoM.calculateCombinedCosts(EmiPlayerInventory.of(client.player)); + } else { + for (MaterialTree tree : trees) { + tree.calculateCost(); + } + } + EmiPersistentData.save(); + if (client.currentScreen instanceof BoMScreen screen) { + screen.init(); + } + } + + public static class TreeBookmark implements EmiIngredient { + private final List trees; + private final List roots; + private final int selectedIndex; + private final boolean craftingMode; + private final String name; + + public static TreeBookmark fromTrees(List trees, int selectedIndex, boolean craftingMode, String name) { + List snaps = TreeSnapshot.ofAll(trees); + if (snaps.isEmpty()) { + return null; + } + return new TreeBookmark(snaps, selectedIndex, craftingMode, name); + } + + private TreeBookmark(List trees, int selectedIndex, boolean craftingMode, String name) { + this.trees = Collections.unmodifiableList(trees); + List roots = Lists.newArrayList(); + for (TreeSnapshot snap : trees) { + EmiIngredient ingredient = snap.rootIngredient(); + if (ingredient != null && !ingredient.isEmpty()) { + roots.add(ingredient); + } + } + this.roots = Collections.unmodifiableList(roots); + if (trees.isEmpty()) { + this.selectedIndex = -1; + } else { + this.selectedIndex = Math.max(0, Math.min(selectedIndex, trees.size() - 1)); + } + this.craftingMode = craftingMode; + this.name = normalizeName(name, trees); + } + + private static String normalizeName(String name, List trees) { + String trimmed = name == null ? "" : name.trim(); + if (!trimmed.isEmpty()) { + return trimmed; + } + String fallback = EmiPort.translatable("emi.tree_bookmark").getString(); + for (TreeSnapshot snap : trees) { + EmiIngredient ingredient = snap.rootIngredient(); + if (ingredient != null && !ingredient.isEmpty() && !ingredient.getEmiStacks().isEmpty()) { + Text text = ingredient.getEmiStacks().get(0).getItemStack().getName(); + if (text != null && !text.getString().isEmpty()) { + String rootName = text.getString(); + if (trees.size() > 1) { + return rootName + " +" + (trees.size() - 1); + } + return rootName; + } + } + } + if (trees.size() > 1) { + return fallback + " " + trees.size(); + } + return fallback; + } + + public JsonObject serialize() { + JsonObject json = new JsonObject(); + json.addProperty("name", name); + json.addProperty("selected", selectedIndex); + json.addProperty("crafting", craftingMode); + JsonArray treesArr = new JsonArray(); + for (TreeSnapshot snap : trees) { + treesArr.add(snap.serialize()); + } + json.add("trees", treesArr); + return json; + } + + public static TreeBookmark deserialize(JsonObject obj) { + if (!JsonHelper.hasArray(obj, "trees")) { + return null; + } + String name = JsonHelper.getString(obj, "name", null); + int selected = JsonHelper.getInt(obj, "selected", -1); + boolean crafting = JsonHelper.getBoolean(obj, "crafting", false); + List snaps = Lists.newArrayList(); + for (JsonElement el : JsonHelper.getArray(obj, "trees")) { + if (el.isJsonObject()) { + TreeSnapshot snap = TreeSnapshot.deserialize(el.getAsJsonObject()); + if (snap != null) { + snaps.add(snap); + } + } + } + if (snaps.isEmpty()) { + return null; + } + return new TreeBookmark(snaps, selected, crafting, name); + } + + public TreeBookmark withName(String name) { + return new TreeBookmark(trees, selectedIndex, craftingMode, name); + } + + public List instantiateTrees() { + List instantiated = Lists.newArrayList(); + for (TreeSnapshot snap : trees) { + MaterialNode node = snap.toNode(); + if (node == null) { + continue; + } + MaterialTree tree = new MaterialTree(node, snap.batches); + snap.applyResolutions(tree.resolutions); + applyNodeResolutions(tree.goal, tree.resolutions); + tree.recalculate(); + tree.cost = new TreeCost(); + tree.calculateCost(); + instantiated.add(tree); + } + return instantiated; + } + + public int getSelectedIndex() { + return selectedIndex; + } + + public boolean isCraftingMode() { + return craftingMode; + } + + public String getName() { + return name; + } + + private long getRootAmount(TreeSnapshot snap) { + if (snap.root == null) { + return 0; + } + if (snap.root.catalyst) { + return snap.root.amount; + } + return snap.root.amount * snap.batches; + } + + @Override + public void render(DrawContext draw, int x, int y, float delta, int flags) { + if (trees.isEmpty() || roots.isEmpty()) { + return; + } + int idx = (int) (System.currentTimeMillis() / 1000 % trees.size()); + TreeSnapshot snap = trees.get(idx); + EmiIngredient ingredient = roots.get(idx % roots.size()); + if ((flags & RENDER_ICON) != 0) { + ingredient.render(draw, x, y, delta, flags & ~RENDER_AMOUNT); + } + if ((flags & RENDER_INGREDIENT) != 0) { + EmiRender.renderIngredientIcon(this, draw, x, y); + } + if ((flags & RENDER_AMOUNT) != 0) { + EmiDrawContext context = EmiDrawContext.wrap(draw); + long amount = getRootAmount(snap); + EmiRenderHelper.renderAmount(context, x, y, + EmiRenderHelper.getAmountText(ingredient, amount)); + } + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + public EmiIngredient copy() { + return new TreeBookmark(trees, selectedIndex, craftingMode, name); + } + + @Override + public long getAmount() { + return 1; + } + + @Override + public EmiIngredient setAmount(long amount) { + return null; + } + + @Override + public float getChance() { + return 1; + } + + @Override + public EmiIngredient setChance(float chance) { + return null; + } + + @Override + public List getEmiStacks() { + return EmiStack.EMPTY.getEmiStacks(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public List getTooltip() { + List tooltip = new ArrayList<>(); + tooltip.add(new EmiTextTooltipWrapper(this, EmiPort.ordered(EmiPort.literal(name)))); + tooltip.add(new EmiTextTooltipWrapper(this, + EmiPort.ordered(EmiPort.translatable("emi.tree_bookmark.count", trees.size())))); + if (!roots.isEmpty()) { + tooltip.add(new IngredientTooltipComponent(roots)); + } + return tooltip; + } + } + + private static void applyNodeResolutions(MaterialNode node, Map resolutions) { + applyNodeResolutions(node, resolutions, new HashSet<>()); + } + + private static void applyNodeResolutions(MaterialNode node, Map resolutions, Set path) { + if (node == null) { + return; + } + if (!path.add(node.ingredient)) { + return; + } + if (resolutions.containsKey(node.ingredient)) { + node.recipe = resolutions.get(node.ingredient); + if (node.recipe != null && (node.children == null || node.children.isEmpty())) { + node.defineRecipe(node.recipe); + } + } + if (node.children != null) { + for (MaterialNode child : node.children) { + applyNodeResolutions(child, resolutions, path); + } + } + path.remove(node.ingredient); + } + + private static class TreeSnapshot { + private final MaterialNodeSnapshot root; + private final long batches; + private final List resolutions; + + private TreeSnapshot(MaterialNodeSnapshot root, long batches, List resolutions) { + this.root = root; + this.batches = Math.max(1, batches); + this.resolutions = Collections.unmodifiableList(resolutions); + } + + public static List ofAll(List trees) { + List snaps = Lists.newArrayList(); + for (MaterialTree tree : trees) { + TreeSnapshot snap = of(tree); + if (snap != null) { + snaps.add(snap); + } + } + return snaps; + } + + public static TreeSnapshot of(MaterialTree tree) { + if (tree == null || tree.goal == null) { + return null; + } + MaterialNodeSnapshot root = MaterialNodeSnapshot.of(tree.goal); + if (root == null) { + return null; + } + List resolutions = Lists.newArrayList(); + for (Map.Entry entry : tree.resolutions.entrySet()) { + ResolutionSnapshot snap = ResolutionSnapshot.of(entry.getKey(), entry.getValue()); + if (snap != null) { + resolutions.add(snap); + } + } + return new TreeSnapshot(root, tree.batches, resolutions); + } + + public JsonObject serialize() { + JsonObject json = new JsonObject(); + json.add("root", root.serialize()); + json.addProperty("batches", batches); + if (!resolutions.isEmpty()) { + JsonArray arr = new JsonArray(); + for (ResolutionSnapshot snap : resolutions) { + JsonObject obj = snap.serialize(); + if (obj != null) { + arr.add(obj); + } + } + json.add("resolutions", arr); + } + return json; + } + + public static TreeSnapshot deserialize(JsonObject json) { + if (!JsonHelper.hasJsonObject(json, "root")) { + return null; + } + MaterialNodeSnapshot root = MaterialNodeSnapshot.deserialize(JsonHelper.getObject(json, "root", new JsonObject())); + if (root == null) { + return null; + } + long batches = JsonHelper.getLong(json, "batches", 1); + List resolutions = Lists.newArrayList(); + if (JsonHelper.hasArray(json, "resolutions")) { + for (JsonElement el : JsonHelper.getArray(json, "resolutions")) { + if (el.isJsonObject()) { + ResolutionSnapshot snap = ResolutionSnapshot.deserialize(el.getAsJsonObject()); + if (snap != null) { + resolutions.add(snap); + } + } + } + } + return new TreeSnapshot(root, batches, resolutions); + } + + public MaterialNode toNode() { + return root.toNode(); + } + + public void applyResolutions(Map map) { + root.applyResolutions(map); + for (ResolutionSnapshot snap : resolutions) { + snap.apply(map); + } + } + + public EmiIngredient rootIngredient() { + return root.ingredient; + } + } + + private static class ResolutionSnapshot { + private final EmiIngredient ingredient; + private final String recipeId; + private final EmiStack stack; + private final boolean cleared; + + private ResolutionSnapshot(EmiIngredient ingredient, String recipeId, EmiStack stack, boolean cleared) { + this.ingredient = ingredient; + this.recipeId = recipeId; + this.stack = stack; + this.cleared = cleared; + } + + public static ResolutionSnapshot of(EmiIngredient ingredient, EmiRecipe recipe) { + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + EmiIngredient copy = ingredient.copy(); + if (recipe == null) { + return new ResolutionSnapshot(copy, null, EmiStack.EMPTY, true); + } + String recipeId = recipe.getId() != null ? recipe.getId().toString() : null; + EmiStack stack = recipe instanceof EmiResolutionRecipe err ? err.stack : EmiStack.EMPTY; + return new ResolutionSnapshot(copy, recipeId, stack, false); + } + + public JsonObject serialize() { + JsonObject json = new JsonObject(); + JsonElement ingredient = EmiIngredientSerializer.getSerialized(this.ingredient); + if (ingredient != null) { + json.add("ingredient", ingredient); + } + if (recipeId != null) { + json.addProperty("recipe", recipeId); + } + if (!stack.isEmpty()) { + JsonElement res = EmiIngredientSerializer.getSerialized(stack); + if (res != null) { + json.add("stack", res); + } + } + if (cleared) { + json.addProperty("cleared", true); + } + return json; + } + + public static ResolutionSnapshot deserialize(JsonObject json) { + EmiIngredient ingredient = EmiIngredientSerializer.getDeserialized(json.get("ingredient")); + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + String recipeId = JsonHelper.getString(json, "recipe", null); + EmiStack stack = EmiStack.EMPTY; + if (json.has("stack")) { + EmiIngredient res = EmiIngredientSerializer.getDeserialized(json.get("stack")); + if (res instanceof EmiStack es) { + stack = es; + } else if (res != null && !res.getEmiStacks().isEmpty()) { + stack = res.getEmiStacks().get(0); + } + } + boolean cleared = JsonHelper.getBoolean(json, "cleared", false); + return new ResolutionSnapshot(ingredient.copy(), recipeId, stack, cleared); + } + + public void apply(Map map) { + EmiRecipe recipe = null; + if (recipeId != null && Identifier.tryParse(recipeId) != null) { + recipe = EmiApi.getRecipeManager().getRecipe(EmiPort.id(recipeId)); + } else if (!stack.isEmpty()) { + recipe = new EmiResolutionRecipe(ingredient, stack); + } else if (!cleared) { + return; + } + map.put(ingredient, recipe); + } + } + + private static class MaterialNodeSnapshot { + private final EmiIngredient ingredient; + private final String recipeId; + private final float consumeChance; + private final float produceChance; + private final long amount; + private final long divisor; + private final long remainderAmount; + private final boolean catalyst; + private final FoldState state; + private final ProgressState progress; + private final long neededBatches; + private final long totalNeeded; + private final List children; + + private MaterialNodeSnapshot(EmiIngredient ingredient, String recipeId, float consumeChance, float produceChance, long amount, + long divisor, long remainderAmount, boolean catalyst, FoldState state, ProgressState progress, + long neededBatches, long totalNeeded, List children) { + this.ingredient = ingredient; + this.recipeId = recipeId; + this.consumeChance = consumeChance; + this.produceChance = produceChance; + this.amount = amount; + this.divisor = divisor; + this.remainderAmount = remainderAmount; + this.catalyst = catalyst; + this.state = state; + this.progress = progress; + this.neededBatches = neededBatches; + this.totalNeeded = totalNeeded; + this.children = children; + } + + public static MaterialNodeSnapshot of(MaterialNode node) { + if (node == null || node.ingredient == null || node.ingredient.isEmpty()) { + return null; + } + List children = Lists.newArrayList(); + if (node.children != null) { + for (MaterialNode child : node.children) { + MaterialNodeSnapshot snap = of(child); + if (snap != null) { + children.add(snap); + } + } + } + String recipeId = node.recipe != null && node.recipe.getId() != null ? node.recipe.getId().toString() : null; + return new MaterialNodeSnapshot(node.ingredient.copy(), recipeId, node.consumeChance, node.produceChance, + node.amount, node.divisor, node.remainderAmount, node.catalyst, node.state, node.progress, + node.neededBatches, node.totalNeeded, children); + } + + public JsonObject serialize() { + JsonObject json = new JsonObject(); + JsonElement ingredient = EmiIngredientSerializer.getSerialized(this.ingredient); + if (ingredient != null) { + json.add("ingredient", ingredient); + } + if (recipeId != null) { + json.addProperty("recipe", recipeId); + } + json.addProperty("consume_chance", consumeChance); + json.addProperty("produce_chance", produceChance); + json.addProperty("amount", amount); + json.addProperty("divisor", divisor); + json.addProperty("remainder_amount", remainderAmount); + json.addProperty("catalyst", catalyst); + json.addProperty("state", state.name()); + json.addProperty("progress", progress.name()); + json.addProperty("needed_batches", neededBatches); + json.addProperty("total_needed", totalNeeded); + JsonArray children = new JsonArray(); + for (MaterialNodeSnapshot child : this.children) { + children.add(child.serialize()); + } + json.add("children", children); + return json; + } + + public static MaterialNodeSnapshot deserialize(JsonObject json) { + EmiIngredient ingredient = EmiIngredientSerializer.getDeserialized(json.get("ingredient")); + if (ingredient == null || ingredient.isEmpty()) { + return null; + } + String recipeId = JsonHelper.getString(json, "recipe", null); + float consumeChance = JsonHelper.getFloat(json, "consume_chance", 1f); + float produceChance = JsonHelper.getFloat(json, "produce_chance", 1f); + long amount = JsonHelper.getLong(json, "amount", ingredient.getAmount()); + long divisor = JsonHelper.getLong(json, "divisor", 1); + long remainderAmount = JsonHelper.getLong(json, "remainder_amount", 0); + boolean catalyst = JsonHelper.getBoolean(json, "catalyst", false); + FoldState state = FoldState.valueOf(JsonHelper.getString(json, "state", FoldState.EXPANDED.name())); + ProgressState progress = ProgressState.valueOf(JsonHelper.getString(json, "progress", ProgressState.UNSTARTED.name())); + long neededBatches = JsonHelper.getLong(json, "needed_batches", 0); + long totalNeeded = JsonHelper.getLong(json, "total_needed", 0); + List children = Lists.newArrayList(); + if (JsonHelper.hasArray(json, "children")) { + for (JsonElement el : JsonHelper.getArray(json, "children")) { + if (el.isJsonObject()) { + MaterialNodeSnapshot snap = deserialize(el.getAsJsonObject()); + if (snap != null) { + children.add(snap); + } + } + } + } + return new MaterialNodeSnapshot(ingredient, recipeId, consumeChance, produceChance, amount, divisor, remainderAmount, + catalyst, state, progress, neededBatches, totalNeeded, children); + } + + public MaterialNode toNode() { + MaterialNode node = new MaterialNode(ingredient); + node.recipe = recipeFromId(); + node.consumeChance = consumeChance; + node.produceChance = produceChance; + node.amount = amount; + node.divisor = divisor; + node.remainderAmount = remainderAmount; + node.catalyst = catalyst; + node.state = state; + node.progress = progress; + node.neededBatches = neededBatches; + node.totalNeeded = totalNeeded; + if (!children.isEmpty()) { + node.children = Lists.newArrayList(); + for (MaterialNodeSnapshot child : children) { + MaterialNode materialNode = child.toNode(); + if (materialNode != null) { + node.children.add(materialNode); + } + } + } + if (node.recipe != null && (node.children == null || node.children.isEmpty())) { + node.defineRecipe(node.recipe); + } + return node; + } + + private EmiRecipe recipeFromId() { + if (recipeId == null || Identifier.tryParse(recipeId) == null) { + return null; + } + return EmiApi.getRecipeManager().getRecipe(EmiPort.id(recipeId)); + } + + public void applyResolutions(Map map) { + if (recipeId != null) { + EmiRecipe recipe = recipeFromId(); + if (recipe != null) { + map.put(ingredient, recipe); + } + } + for (MaterialNodeSnapshot child : children) { + child.applyResolutions(map); + } + } + } +} \ No newline at end of file diff --git a/xplat/src/main/java/dev/emi/emi/screen/BoMScreen.java b/xplat/src/main/java/dev/emi/emi/screen/BoMScreen.java index 727944ca..9d393c3e 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/BoMScreen.java +++ b/xplat/src/main/java/dev/emi/emi/screen/BoMScreen.java @@ -1,5 +1,6 @@ package dev.emi.emi.screen; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -7,7 +8,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.joml.Matrix4f; import org.joml.Matrix4fStack; import org.lwjgl.glfw.GLFW; @@ -32,6 +32,7 @@ import dev.emi.emi.bom.FlatMaterialCost; import dev.emi.emi.bom.FoldState; import dev.emi.emi.bom.MaterialNode; +import dev.emi.emi.bom.MaterialTree; import dev.emi.emi.bom.ProgressState; import dev.emi.emi.bom.TreeCost; import dev.emi.emi.config.EmiConfig; @@ -42,9 +43,11 @@ import dev.emi.emi.runtime.EmiDrawContext; import dev.emi.emi.runtime.EmiFavorites; import dev.emi.emi.runtime.EmiHistory; +import dev.emi.emi.runtime.EmiTreeBookmarks; import dev.emi.emi.screen.StackBatcher.Batchable; import dev.emi.emi.screen.tooltip.EmiTooltip; import dev.emi.emi.screen.tooltip.RecipeTooltipComponent; +import dev.emi.emi.screen.TreeBookmarkNameScreen; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; @@ -79,6 +82,15 @@ public class BoMScreen extends Screen { private int nodeHeight = 0; private int lastMouseX, lastMouseY; private double scrollAcc = 0; + private Bounds rootLeft = new Bounds(0, 0, 12, 12); + private Bounds rootRight = new Bounds(0, 0, 12, 12); + private Bounds rootArea = new Bounds(0, 0, 0, 0); + private List rootSlots = Lists.newArrayList(); + private List rootIndices = Lists.newArrayList(); + private List rootAmounts = Lists.newArrayList(); + private int rootScroll = 0; + private boolean initialViewSet = false; + private boolean altDown = false; public BoMScreen(HandledScreen old) { super(EmiPort.translatable("screen.emi.recipe_tree")); @@ -86,8 +98,8 @@ public BoMScreen(HandledScreen old) { } public void init() { - if (BoM.tree != null) { - offY = height / -3; + if (BoM.getTree() != null) { + offY = 0; } else { offY = 0; } @@ -96,8 +108,14 @@ public void init() { public void recalculateTree() { help = new Bounds(width - 18, height - 18, 16, 16); - if (BoM.tree != null) { - TreeVolume volume = addNewNodes(BoM.tree.goal, BoM.tree.batches, 1, 0, ChanceState.DEFAULT); + MaterialTree tree = BoM.getTree(); + List roots = BoM.getTrees(); + rootSlots.clear(); + rootIndices.clear(); + rootAmounts.clear(); + int cy = 0; + if (tree != null) { + TreeVolume volume = addNewNodes(tree.goal, tree.batches, 1, 0, ChanceState.DEFAULT); nodes = volume.nodes; int horizontalOffset = (volume.getMaxRight() + volume.getMinLeft()) / 2; for (Node node : volume.nodes) { @@ -105,30 +123,29 @@ public void recalculateTree() { } if (!volume.nodes.isEmpty()) { Node node = volume.nodes.get(0); - int width = textRenderer.getWidth("x" + BoM.tree.batches); + int width = textRenderer.getWidth("x" + tree.batches); batches = new Bounds(node.x + node.width / 2 + 6, node.y - 10, width + 12, 22); } nodeWidth = volume.getMaxRight() - volume.getMinLeft(); - nodeHeight = getNodeHeight(BoM.tree.goal); + nodeHeight = getNodeHeight(tree.goal); playerInv = EmiPlayerInventory.of(client.player); - BoM.tree.calculateProgress(playerInv); - Map progressCosts = BoM.tree.cost.costs.values().stream() + BoM.calculateCombinedCosts(playerInv); + Map progressCosts = BoM.combinedProgress.costs.values().stream() .collect(Collectors.toMap(c -> c.ingredient, c -> c)); - Map chanceProgressCosts = BoM.tree.cost.chanceCosts.values().stream() + Map chanceProgressCosts = BoM.combinedProgress.chanceCosts.values().stream() .collect(Collectors.toMap(c -> c.ingredient, c -> c)); - + costs.clear(); - BoM.tree.calculateCost(); List treeCosts = Stream.concat( - BoM.tree.cost.costs.values().stream(), - BoM.tree.cost.chanceCosts.values().stream() + BoM.combinedCost.costs.values().stream(), + BoM.combinedCost.chanceCosts.values().stream() ).sorted((a, b) -> Integer.compare( EmiStackList.getIndex(a.ingredient.getEmiStacks().get(0)), EmiStackList.getIndex(b.ingredient.getEmiStacks().get(0)) )).toList(); - int cy = nodeHeight * NODE_VERTICAL_SPACING * 2; + cy = nodeHeight * NODE_VERTICAL_SPACING * 2; int costX = 0; for (FlatMaterialCost node : treeCosts) { Cost cost = new Cost(node, costX, cy, false); @@ -163,8 +180,8 @@ public void recalculateTree() { List remainders = Lists.newArrayList(); List remainderCosts = Stream.concat( - BoM.tree.cost.remainders.values().stream(), - BoM.tree.cost.chanceRemainders.values().stream() + BoM.combinedCost.remainders.values().stream(), + BoM.combinedCost.chanceRemainders.values().stream() ).sorted((a, b) -> Integer.compare( EmiStackList.getIndex(a.ingredient.getEmiStacks().get(0)), EmiStackList.getIndex(b.ingredient.getEmiStacks().get(0)) @@ -187,10 +204,91 @@ public void recalculateTree() { hasRemainders = !remainders.isEmpty(); } else { nodes = Lists.newArrayList(); + costs.clear(); + BoM.combinedCost.clear(); + BoM.combinedProgress.clear(); + hasRemainders = false; + } + + int rootY = -NODE_VERTICAL_SPACING * 3; + int maxVisible = 7; + rootScroll = Math.max(0, Math.min(rootScroll, Math.max(0, roots.size() - maxVisible))); + if (BoM.treeIndex >= 0) { + if (BoM.treeIndex < rootScroll) { + rootScroll = BoM.treeIndex; + } + if (BoM.treeIndex >= rootScroll + maxVisible) { + rootScroll = BoM.treeIndex - maxVisible + 1; + } } + int visible = Math.min(maxVisible, roots.size()); + int rowWidth = visible > 0 ? ((visible - 1) * 20 + 16) : 0; + int startX = visible > 0 ? -((visible - 1) * 20) / 2 : 0; + rootLeft = new Bounds(startX - 20, rootY - 4, 12, 12); + rootRight = new Bounds(startX + rowWidth - 8, rootY - 4, 12, 12); + rootArea = new Bounds(startX - 24, rootY - 12, rowWidth + 48, 24); + for (int i = 0; i < visible; i++) { + int index = i + rootScroll; + int x = startX + i * 20; + rootSlots.add(new Bounds(x - 8, rootY - 8, 16, 16)); + rootIndices.add(index); + rootAmounts.add(getRootAmount(roots.get(index))); + } + if (!initialViewSet && tree != null) { + adjustInitialView(cy); + initialViewSet = true; + } + ensureRootVisible(); batcher.repopulate(); } + private void adjustInitialView(int totalCostY) { + int top = rootArea.y() - 12; + int bottom = totalCostY + 16 + (hasRemainders ? 40 : 0); + if (rootArea.height() <= 0) { + return; + } + if (zoom == 0) { + int margin = 16; + int requiredHeight = bottom - top + margin * 2; + int guiScale = (int) this.client.getWindow().getScaleFactor(); + float desiredScale = Math.min(1f, (float) this.height / requiredHeight); + int targetDesired = Math.max(1, Math.round(guiScale * desiredScale)); + int targetZoom = Math.max(-6, Math.min(4, targetDesired - guiScale)); + zoom = targetZoom; + } + float scale = getScale(); + int visibleHeight = (int) (height / scale); + int margin = 16; + int middle = (top + bottom) / 2; + int min = -visibleHeight / 2 + margin - bottom; + int max = visibleHeight / 2 - margin - top; + offY = MathHelper.clamp(-middle, min, max); + } + + private void ensureRootVisible() { + if (rootArea == null) { + return; + } + float scale = getScale(); + int scaledHeight = (int) (height / scale); + int margin = 16; + int min = -scaledHeight / 2 + margin - (rootArea.y() + rootArea.height()); + int max = scaledHeight / 2 - margin - rootArea.y(); + offY = MathHelper.clamp(offY, min, max); + } + + private long getRootAmount(MaterialTree tree) { + MaterialNode goal = tree.goal; + if (goal == null) { + return 0; + } + if (goal.catalyst) { + return goal.amount; + } + return goal.amount * tree.batches; + } + @Override public void render(DrawContext raw, int mouseX, int mouseY, float delta) { EmiDrawContext context = EmiDrawContext.wrap(raw); @@ -212,6 +310,8 @@ public void render(DrawContext raw, int mouseX, int mouseY, float delta) { int mx = (int) ((mouseX - width / 2) / scale - offX); int my = (int) ((mouseY - height / 2) / scale - offY); + MaterialTree tree = BoM.getTree(); + List roots = BoM.getTrees(); Matrix4fStack view = RenderSystem.getModelViewStack(); view.pushMatrix(); @@ -219,24 +319,68 @@ public void render(DrawContext raw, int mouseX, int mouseY, float delta) { view.scale(scale, scale, 1); view.translate((float)offX, (float)offY, 0); EmiPort.applyModelViewMatrix(); - if (BoM.tree != null) { + if (tree != null) { batcher.begin(0, 0, 0); + for (int i = 0; i < rootSlots.size(); i++) { + Bounds slot = rootSlots.get(i); + int index = rootIndices.get(i); + MaterialTree rootTree = roots.get(index); + Bounds del = new Bounds(slot.x(), slot.y() - 10, slot.width(), 8); + boolean hoveredSlot = slot.contains(mx, my); + boolean hoveredDelete = del.contains(mx, my); + boolean selected = index == BoM.treeIndex; + int border = selected ? 0xff8099ff : 0xffffffff; + if (!selected && hoveredSlot) { + border = 0xffc0d0ff; + } + int lx = slot.x() - 1; + int ly = slot.y() - 1; + int bw = slot.width() + 2; + int bh = slot.height() + 2; + context.fill(lx, ly, bw, 1, border); + context.fill(lx, ly + bh - 1, bw, 1, border); + context.fill(lx, ly, 1, bh, border); + context.fill(lx + bw - 1, ly, 1, bh, border); + context.setColor(1f, 1f, 1f, 1f); + rootTree.goal.ingredient.render(context.raw(), slot.x(), slot.y(), delta, + ~(EmiIngredient.RENDER_AMOUNT | EmiIngredient.RENDER_REMAINDER)); + if (i < rootAmounts.size()) { + EmiRenderHelper.renderAmount(context, slot.x(), slot.y(), + EmiRenderHelper.getAmountText(rootTree.goal.ingredient, rootAmounts.get(i))); + } + if (hoveredSlot || hoveredDelete) { + context.fill(del.x(), del.y(), del.width(), del.height(), 0x88ff0000); + context.drawCenteredText(EmiPort.literal("X"), del.x() + del.width() / 2, del.y() + 1); + } + } + if (roots.size() > rootSlots.size()) { + if (rootLeft.contains(mx, my)) { + context.setColor(0.5f, 0.6f, 1f, 1f); + } + context.drawCenteredText(EmiPort.literal("<"), rootLeft.x() + rootLeft.width() / 2, rootLeft.y()); + context.setColor(1f, 1f, 1f, 1f); + if (rootRight.contains(mx, my)) { + context.setColor(0.5f, 0.6f, 1f, 1f); + } + context.drawCenteredText(EmiPort.literal(">"), rootRight.x() + rootRight.width() / 2, rootRight.y()); + context.setColor(1f, 1f, 1f, 1f); + } int cy = nodeHeight * NODE_VERTICAL_SPACING * 2; context.drawCenteredText(EmiPort.translatable("emi.total_cost"), 0, cy - 16); if (hasRemainders) { context.drawCenteredText(EmiPort.translatable("emi.leftovers"), 0, cy - 16 + 40); } for (Cost cost : costs) { - cost.render(context); + cost.renderBase(context); } for (Node node : nodes) { - node.render(context, mx, my, delta); + node.renderBase(context, mx, my, delta); } int color = -1; if (batches.contains(mx, my)) { color = 0xff8099ff; } - context.drawTextWithShadow(EmiPort.literal("x" + BoM.tree.batches), + context.drawTextWithShadow(EmiPort.literal("x" + tree.batches), batches.x() + 6, batches.y() + batches.height() / 2 - 4, color); if (mode.contains(mx, my)) { @@ -245,6 +389,12 @@ public void render(DrawContext raw, int mouseX, int mouseY, float delta) { context.drawTexture(EmiRenderHelper.WIDGETS, mode.x(), mode.y(), BoM.craftingMode ? 16 : 0, 146, mode.width(), mode.height()); context.setColor(1f, 1f, 1f, 1f); batcher.draw(); + for (Cost cost : costs) { + cost.renderAmount(context); + } + for (Node node : nodes) { + node.renderAmount(context); + } } else { context.drawCenteredText(EmiPort.translatable("emi.tree_welcome", EmiRenderHelper.getEmiText()), 0, -72); context.drawCenteredText(EmiPort.translatable("emi.no_tree"), 0, -48); @@ -264,17 +414,19 @@ public void render(DrawContext raw, int mouseX, int mouseY, float delta) { Hover hover = getHoveredStack(mouseX, mouseY); if (hover != null) { hover.drawTooltip(this, context, mouseX, mouseY); - } else if (BoM.tree != null && batches.contains(mx, my)) { + } else if (tree != null && batches.contains(mx, my)) { List list = Lists.newArrayList(); - list.addAll(EmiTooltip.splitTranslate("tooltip.emi.bom.batch_size", BoM.tree.batches)); + list.addAll(EmiTooltip.splitTranslate("tooltip.emi.bom.batch_size", tree.batches)); list.add(EmiTooltipComponents.of(EmiPort.translatable("tooltip.emi.bom.batch_size.ideal", EmiBind.LEFT_CLICK.getBindText()))); + list.add(EmiTooltipComponents.of(EmiPort.translatable("tooltip.emi.bom.batch_size.multiply"))); + list.add(EmiTooltipComponents.of(EmiPort.translatable("tooltip.emi.bom.batch_size.all"))); EmiRenderHelper.drawTooltip(this, context, list, mouseX, mouseY); - } else if (BoM.tree != null && mode.contains(mx, my)) { + } else if (tree != null && mode.contains(mx, my)) { String key = BoM.craftingMode ? "tooltip.emi.bom.mode.craft" : "tooltip.emi.bom.mode.view"; - List list = EmiTooltip.splitTranslate(key, BoM.tree.batches); + List list = EmiTooltip.splitTranslate(key, tree.batches); EmiRenderHelper.drawTooltip(this, context, list, mouseX, mouseY); } else if (help.contains(mouseX, mouseY)) { - List list = EmiTooltip.splitTranslate("tooltip.emi.bom.help"); + List list = Collections.singletonList(TooltipComponent.of(EmiPort.ordered(EmiPort.translatable("tooltip.emi.bom.help", EmiConfig.addTreeBookmark.getBindText())))); EmiRenderHelper.drawTooltip(this, context, list, width - 18, height - 18, width); } } @@ -396,9 +548,21 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { } } } else if (EmiInput.isControlDown() && keyCode == GLFW.GLFW_KEY_C) { - BoM.tree = null; + BoM.getTrees().clear(); + BoM.treeIndex = -1; + BoM.craftingMode = false; init(); } + if (EmiConfig.addTreeBookmark.matchesKey(keyCode, scanCode)) { + String suggested = EmiTreeBookmarks.suggestName(BoM.getTrees(), BoM.treeIndex, BoM.craftingMode); + MinecraftClient.getInstance().setScreen(new TreeBookmarkNameScreen(this, suggested, name -> + EmiTreeBookmarks.addBookmark(BoM.getTrees(), BoM.treeIndex, BoM.craftingMode, name))); + return true; + } + if (EmiInput.isAltDown() != altDown) { + altDown = EmiInput.isAltDown(); + recalculateTree(); + } return super.keyPressed(keyCode, scanCode, modifiers); } @@ -440,10 +604,38 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { float scale = getScale(); int mx = (int) ((mouseX - width / 2) / scale - offX); int my = (int) ((mouseY - height / 2) / scale - offY); + MaterialTree tree = BoM.getTree(); + List roots = BoM.getTrees(); + for (int i = 0; i < rootSlots.size(); i++) { + Bounds slot = rootSlots.get(i); + int index = rootIndices.get(i); + Bounds del = new Bounds(slot.x(), slot.y() - 10, slot.width(), 8); + if (del.contains(mx, my)) { + BoM.removeTree(index); + recalculateTree(); + return true; + } + if (slot.contains(mx, my)) { + BoM.selectTree(index); + recalculateTree(); + return true; + } + } + if (roots.size() > rootSlots.size()) { + if (rootLeft.contains(mx, my)) { + BoM.cycleTree(-1); + recalculateTree(); + return true; + } else if (rootRight.contains(mx, my)) { + BoM.cycleTree(1); + recalculateTree(); + return true; + } + } if (hover != null) { if (button == 1 && hover.node != null && hover.node.recipe != null) { if (EmiInput.isShiftDown()) { - BoM.tree.addResolution(hover.node.ingredient, null); + BoM.addResolution(hover.node.ingredient, null); } else if (!(hover.node.recipe instanceof EmiResolutionRecipe)) { if (hover.node.state == FoldState.EXPANDED) { hover.node.state = FoldState.COLLAPSED; @@ -456,7 +648,7 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { } if (hover.stack != null) { if (EmiInput.isShiftDown() && button == 0) { - if (getAutoResolutions(hover, BoM.tree::addResolution)) { + if (getAutoResolutions(hover, BoM::addResolution)) { recalculateTree(); } return true; @@ -481,11 +673,11 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0f)); BoM.craftingMode = !BoM.craftingMode; recalculateTree(); - } else if (batches.contains(mx, my) && BoM.tree != null) { - long ideal = BoM.tree.cost.getIdealBatch(BoM.tree.goal, 1, 1); - if (ideal != BoM.tree.batches) { + } else if (batches.contains(mx, my) && tree != null) { + long ideal = tree.cost.getIdealBatch(tree.goal, 1, 1); + if (ideal != tree.batches) { MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0f)); - BoM.tree.batches = ideal; + tree.batches = ideal; recalculateTree(); } } @@ -505,24 +697,43 @@ public boolean mouseScrolled(double mouseX, double mouseY, double horizontal, do float scale = getScale(); int mx = (int) ((mouseX - width / 2) / scale - offX); int my = (int) ((mouseY - height / 2) / scale - offY); - if (BoM.tree != null && batches.contains(mx, my)) { - long adjustment = (long) amount; - if (EmiInput.isShiftDown()) { - adjustment *= 16; - } else if (EmiInput.isControlDown()) { - if (amount > 0) { - adjustment = BoM.tree.batches; + MaterialTree tree = BoM.getTree(); + List roots = BoM.getTrees(); + if (!roots.isEmpty() && rootArea.contains(mx, my) && amount != 0) { + BoM.cycleTree(-(int) amount); + recalculateTree(); + return true; + } + if (tree != null && batches.contains(mx, my)) { + long scroll = (long) amount; + List targets = EmiInput.isAltDown() ? roots : List.of(tree); + boolean changed = false; + for (MaterialTree target : targets) { + long adjustment = scroll; + if (EmiInput.isShiftDown()) { + adjustment *= 16; + } else if (EmiInput.isControlDown()) { + if (scroll > 0) { + adjustment = target.batches; + } else { + adjustment = -target.batches / 2; + } + } + long newBatches; + if (target.batches == 1 && adjustment > 1) { + newBatches = adjustment; } else { - adjustment = -BoM.tree.batches / 2; + newBatches = target.batches + adjustment; + } + newBatches = Math.max(1, newBatches); + if (newBatches != target.batches) { + target.batches = newBatches; + changed = true; } } - if (BoM.tree.batches == 1 && adjustment > 1) { - BoM.tree.batches = adjustment; - } else { - BoM.tree.batches += adjustment; + if (changed) { + recalculateTree(); } - BoM.tree.batches = Math.max(1, BoM.tree.batches); - recalculateTree(); return true; } zoom += (int) amount; @@ -563,8 +774,11 @@ public Cost(FlatMaterialCost cost, int x, int y, boolean remainder) { this.remainder = remainder; } - public void render(EmiDrawContext context) { + public void renderBase(EmiDrawContext context) { batcher.render(cost.ingredient, context.raw(), x, y, 0, ~(EmiIngredient.RENDER_AMOUNT | EmiIngredient.RENDER_REMAINDER)); + } + + public void renderAmount(EmiDrawContext context) { EmiRenderHelper.renderAmount(context, x, y, getAmountText()); } @@ -674,7 +888,7 @@ public Node(MaterialNode node, long amount, int x, int y, ChanceState chance) { midOffset = tw / -2; } - public void render(EmiDrawContext context, int mouseX, int mouseY, float delta) { + public void renderBase(EmiDrawContext context, int mouseX, int mouseY, float delta) { if (parent != null) { context.push(); @@ -729,6 +943,11 @@ public void render(EmiDrawContext context, int mouseX, int mouseY, float delta) } context.setColor(1f, 1f, 1f, 1f); batcher.render(node.ingredient, context.raw(), x + xo - 8 + midOffset, y - 8, 0); + } + + public void renderAmount(EmiDrawContext context) { + int xo = node.recipe != null ? 11 : 0; + context.setColor(1f, 1f, 1f, 1f); EmiRenderHelper.renderAmount(context, x + xo - 8 + midOffset, y - 8, getAmountText()); } diff --git a/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java b/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java index 2a942e9a..d91ae9db 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java +++ b/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java @@ -9,6 +9,9 @@ import net.minecraft.command.argument.ItemStackArgument; import net.minecraft.component.ComponentChanges; +import dev.emi.emi.api.stack.SearchEmiIngredient; +import dev.emi.emi.runtime.*; +import net.minecraft.client.util.math.MatrixStack; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4fStack; import org.lwjgl.glfw.GLFW; @@ -56,16 +59,6 @@ import dev.emi.emi.registry.EmiRecipeFiller; import dev.emi.emi.registry.EmiRecipes; import dev.emi.emi.registry.EmiStackProviders; -import dev.emi.emi.runtime.EmiDrawContext; -import dev.emi.emi.runtime.EmiFavorite; -import dev.emi.emi.runtime.EmiFavorites; -import dev.emi.emi.runtime.EmiHidden; -import dev.emi.emi.runtime.EmiHistory; -import dev.emi.emi.runtime.EmiLog; -import dev.emi.emi.runtime.EmiProfiler; -import dev.emi.emi.runtime.EmiReloadLog; -import dev.emi.emi.runtime.EmiReloadManager; -import dev.emi.emi.runtime.EmiSidebars; import dev.emi.emi.screen.tooltip.RecipeTooltipComponent; import dev.emi.emi.screen.widget.EmiSearchWidget; import dev.emi.emi.screen.widget.SidebarButtonWidget; @@ -81,7 +74,6 @@ import net.minecraft.client.gui.tooltip.TooltipComponent; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.sound.PositionedSoundInstance; -import net.minecraft.client.util.math.MatrixStack; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; import net.minecraft.screen.slot.Slot; @@ -863,7 +855,7 @@ private static void renderSlotOverlays(EmiDrawContext context, int mouseX, int m } Set ignoredSlots = Sets.newHashSet(); Set synfavs = Sets.newHashSet(); - if (BoM.craftingMode && BoM.tree != null) { + if (BoM.craftingMode && BoM.getTree() != null) { List syntheticFavorites = EmiFavorites.syntheticFavorites; for (EmiFavorite.Synthetic fav : syntheticFavorites) { synfavs.addAll(fav.getEmiStacks()); @@ -892,13 +884,13 @@ private static void renderSlotOverlays(EmiDrawContext context, int mouseX, int m context.push(); context.matrices().translate(0, 0, 300); if (query != null) { - if (!query.test(stack)) { - context.fill(slot.x - 1, slot.y - 1, 18, 18, 0x77000000); - } - } else if (BoM.craftingMode && BoM.tree != null) { - if (!(slot.inventory instanceof PlayerInventory) && !ignoredSlots.contains(slot) && synfavs.contains(stack)) { - context.fill(slot.x - 1, slot.y - 1, 18, 18, 0x7700BBFF); - } + if (!query.test(stack)) { + context.fill(slot.x - 1, slot.y - 1, 18, 18, 0x77000000); + } + } else if (BoM.craftingMode && BoM.getTree() != null) { + if (!(slot.inventory instanceof PlayerInventory) && !ignoredSlots.contains(slot) && synfavs.contains(stack)) { + context.fill(slot.x - 1, slot.y - 1, 18, 18, 0x7700BBFF); + } } context.pop(); } @@ -1071,6 +1063,34 @@ public static boolean mouseReleased(double mouseX, double mouseY, int button) { } } else { EmiStackInteraction hovered = getHoveredStack((int) mouseX, (int) mouseY, !isClickClicky(button)); + + if (panel != null) { + ScreenSpace space = panel.getHoveredSpace(mx, my); + if (space != null) { + if (space.getType() == SidebarType.BOOKMARKS && pressedStack instanceof SearchEmiIngredient bookmark) { + if (button == 1) { + EmiBookmarks.removeBookmark(bookmark); + } else if (bookmark.getContent() != null) { + EmiApi.setSearchText(bookmark.getContent()); + EmiPort.focus(search, true); + } + return true; + } else if (space.getType() == SidebarType.TREE_BOOKMARKS && pressedStack instanceof EmiTreeBookmarks.TreeBookmark treeBookmark) { + if (EmiConfig.renameTreeBookmark.matchesMouse(button)) { + String suggested = treeBookmark.getName(); + client.setScreen(new TreeBookmarkNameScreen(client.currentScreen, suggested, name -> + EmiTreeBookmarks.renameBookmark(treeBookmark, name))); + } else if (button == 1) { + EmiTreeBookmarks.removeBookmark(treeBookmark); + } else { + EmiTreeBookmarks.apply(treeBookmark); + EmiApi.viewRecipeTree(); + } + return true; + } + } + } + if (draggedStack.isEmpty() && stackInteraction(hovered, bind -> bind.matchesMouse(button))) { return true; } @@ -1241,7 +1261,7 @@ public static boolean stackInteraction(EmiStackInteraction stack, Function onSave; + private final String initialName; + private TextFieldWidget nameField; + private ButtonWidget done; + + public TreeBookmarkNameScreen(Screen parent, String initialName, Consumer onSave) { + super(EmiPort.translatable("emi.tree_bookmark.name_title")); + this.parent = parent; + this.initialName = initialName == null ? "" : initialName; + this.onSave = onSave; + } + + @Override + protected void init() { + int fieldX = this.width / 2 - FIELD_WIDTH / 2; + int fieldY = this.height / 2 - 10; + nameField = new TextFieldWidget(textRenderer, fieldX, fieldY, FIELD_WIDTH, 20, EmiPort.translatable("emi.tree_bookmark.name_field")); + nameField.setMaxLength(128); + nameField.setText(initialName); + nameField.setFocused(true); + nameField.setCursorToEnd(false); + addSelectableChild(nameField); + + int buttonWidth = 98; + int buttonsY = fieldY + 28; + done = ButtonWidget.builder(EmiPort.translatable("gui.done"), b -> finish()) + .position(this.width / 2 - buttonWidth - 2, buttonsY) + .size(buttonWidth, 20) + .build(); + ButtonWidget cancel = ButtonWidget.builder(EmiPort.translatable("gui.cancel"), b -> closeScreen()) + .position(this.width / 2 + 2, buttonsY) + .size(buttonWidth, 20) + .build(); + addDrawableChild(done); + addDrawableChild(cancel); + setInitialFocus(nameField); + updateButtonState(); + } + + @Override + public void tick() { + super.tick(); + if (nameField != null) { + nameField.setCursorToEnd(false); + updateButtonState(); + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER) { + finish(); + return true; + } else if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + closeScreen(); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void close() { + closeScreen(); + } + + private void finish() { + if (!done.active) { + return; + } + if (onSave != null) { + onSave.accept(nameField.getText().trim()); + } + closeScreen(); + } + + private void closeScreen() { + MinecraftClient.getInstance().setScreen(parent); + } + + private void updateButtonState() { + done.active = nameField != null && !nameField.getText().trim().isEmpty(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + renderBackground(context, this.width / 2, this.height / 2 - 32, 0); + super.render(context, mouseX, mouseY, delta); + nameField.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(textRenderer, this.title, this.width / 2, this.height / 2 - 32, 0xFFFFFF); + nameField.setFocused(true); + } +} \ No newline at end of file diff --git a/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java b/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java index 89d42ee4..74aba19c 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java +++ b/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java @@ -4,6 +4,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import dev.emi.emi.api.EmiApi; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.runtime.EmiBookmarks; +import dev.emi.emi.screen.EmiScreenManager.SidebarPanel; import org.joml.Matrix4fStack; import org.lwjgl.glfw.GLFW; @@ -20,7 +24,6 @@ import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.resource.language.I18n; -import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.MutableText; import net.minecraft.text.Style; import net.minecraft.util.Formatting; @@ -206,6 +209,23 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { setText(""); return true; } + if (EmiConfig.addBookmark.matchesKey(keyCode, scanCode)) { + String search = EmiApi.getSearchText(); + if (!search.isEmpty()) { + SidebarPanel panel = EmiScreenManager.getSearchPanel(); + + if (panel != null) { + List list = panel.space.getStacks(); + + // Limit to at most 8 items for the bookmark + list = list.subList(0, Math.min(list.size(), 8)); + + if (!list.isEmpty()) { + EmiBookmarks.addBookmark(search, list); + } + } + } + } if ((EmiConfig.focusSearch.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_ESCAPE)) { EmiPort.focus(this, false); diff --git a/xplat/src/main/java/dev/emi/emi/screen/widget/ResolutionButtonWidget.java b/xplat/src/main/java/dev/emi/emi/screen/widget/ResolutionButtonWidget.java index e4d98a7a..2d036888 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/widget/ResolutionButtonWidget.java +++ b/xplat/src/main/java/dev/emi/emi/screen/widget/ResolutionButtonWidget.java @@ -23,8 +23,10 @@ public class ResolutionButtonWidget extends ButtonWidget { public ResolutionButtonWidget(int x, int y, int width, int height, EmiIngredient stack, Supplier hoveredWidget) { super(x, y, width, height, EmiPort.literal(""), button -> { - BoM.tree.addResolution(stack, null); - EmiHistory.pop(); + if (BoM.getTree() != null) { + BoM.addResolution(stack, null); + EmiHistory.pop(); + } }, s -> s.get()); this.stack = stack; this.hoveredWidget = hoveredWidget; diff --git a/xplat/src/main/java/dev/emi/emi/stack/serializer/SearchEmiIngredientSerializer.java b/xplat/src/main/java/dev/emi/emi/stack/serializer/SearchEmiIngredientSerializer.java new file mode 100644 index 00000000..28ae46c1 --- /dev/null +++ b/xplat/src/main/java/dev/emi/emi/stack/serializer/SearchEmiIngredientSerializer.java @@ -0,0 +1,49 @@ +package dev.emi.emi.stack.serializer; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.api.stack.SearchEmiIngredient; +import dev.emi.emi.api.stack.serializer.EmiIngredientSerializer; +import dev.emi.emi.registry.EmiIngredientSerializers; +import net.minecraft.util.JsonHelper; + +import java.util.ArrayList; +import java.util.List; + +public class SearchEmiIngredientSerializer implements EmiIngredientSerializer { + @Override + public String getType() { + return "search"; + } + + @Override + public EmiIngredient deserialize(JsonElement element) { + JsonObject json = element.getAsJsonObject(); + String content = JsonHelper.getString(json, "content"); + JsonArray resultsArray = JsonHelper.getArray(json, "results"); + + List results = new ArrayList<>(); + for (JsonElement resultElement : resultsArray) { + results.add(EmiIngredientSerializers.deserialize(resultElement)); + } + + return EmiIngredient.of(content, results); + } + + @Override + public JsonElement serialize(SearchEmiIngredient stack) { + JsonObject json = new JsonObject(); + json.addProperty("type", getType()); + json.addProperty("content", stack.getContent()); + + JsonArray results = new JsonArray(); + for (EmiIngredient inner : stack.getResults()) { + results.add(EmiIngredientSerializers.serialize(inner)); + } + json.add("results", results); + + return json; + } +} diff --git a/xplat/src/main/java/dev/emi/emi/widget/RecipeTreeButtonWidget.java b/xplat/src/main/java/dev/emi/emi/widget/RecipeTreeButtonWidget.java index a9703e92..257656c0 100644 --- a/xplat/src/main/java/dev/emi/emi/widget/RecipeTreeButtonWidget.java +++ b/xplat/src/main/java/dev/emi/emi/widget/RecipeTreeButtonWidget.java @@ -3,9 +3,13 @@ import java.util.List; import dev.emi.emi.EmiPort; +import dev.emi.emi.EmiRenderHelper; import dev.emi.emi.api.EmiApi; import dev.emi.emi.api.recipe.EmiRecipe; import dev.emi.emi.bom.BoM; +import dev.emi.emi.input.EmiInput; +import dev.emi.emi.runtime.EmiDrawContext; +import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.tooltip.TooltipComponent; public class RecipeTreeButtonWidget extends RecipeButtonWidget { @@ -17,7 +21,7 @@ public RecipeTreeButtonWidget(int x, int y, EmiRecipe recipe) { @Override public int getTextureOffset(int mouseX, int mouseY) { int v = super.getTextureOffset(mouseX, mouseY); - if (BoM.tree != null && BoM.tree.goal.recipe == recipe) { + if (BoM.getTree() != null && BoM.getTree().goal.recipe == recipe) { v += 36; } return v; @@ -30,9 +34,22 @@ public List getTooltip(int mouseX, int mouseY) { @Override public boolean mouseClicked(int mouseX, int mouseY, int button) { - BoM.setGoal(recipe); + if (EmiInput.isShiftDown()) { + BoM.addGoal(recipe); + } else { + BoM.setGoal(recipe); + } this.playButtonSound(); EmiApi.viewRecipeTree(); return true; } + + @Override + public void render(DrawContext raw, int mouseX, int mouseY, float delta) { + EmiDrawContext context = EmiDrawContext.wrap(raw); + context.resetColor(); + int u = EmiInput.isShiftDown() ? 24 : this.u; + int v = this.v + getTextureOffset(mouseX, mouseY); + context.drawTexture(EmiRenderHelper.BUTTONS, x, y, 12, 12, u, v, 12, 12, 256, 256); + } } diff --git a/xplat/src/main/resources/assets/emi/lang/en_us.json b/xplat/src/main/resources/assets/emi/lang/en_us.json index a07d2dcd..11d83dd1 100644 --- a/xplat/src/main/resources/assets/emi/lang/en_us.json +++ b/xplat/src/main/resources/assets/emi/lang/en_us.json @@ -6,6 +6,8 @@ "key.emi.toggle_visibility": "Toggle Visibility", "key.emi.focus_search": "Focus Search", "key.emi.clear_search": "Clear Search", + "key.emi.add_bookmark": "Add Search Bookmark", + "key.emi.add_tree_bookmark": "Add Tree Bookmark", "key.emi.view_recipes": "View Recipes", "key.emi.view_uses": "View Uses", "key.emi.favorite": "Favorite", @@ -29,6 +31,7 @@ "key.emi.copy_recipe_id": "Copy Recipe ID", "key.emi.hide_stack": "Hide Stack", "key.emi.hide_stack_by_id": "Hide Stack by ID", + "key.emi.rename_tree_bookmark": "Rename Tree Bookmark", "emi.index_source.creative": "Creative", "emi.index_source.registered": "Registered", @@ -52,6 +55,9 @@ "emi.sidebar.type.craftables.description": "Displays all craftable stacks", "emi.sidebar.type.favorites": "Favorites", "emi.sidebar.type.favorites.description": "Displays favorites", + "emi.sidebar.type.bookmarks": "Search Bookmarks", + "emi.sidebar.type.tree_bookmarks": "Tree Bookmarks", + "emi.sidebar.type.tree_bookmarks.description": "Displays saved recipe trees", "emi.sidebar.type.lookup_history": "Lookup History", "emi.sidebar.type.lookup_history.description": "Displays previously looked up stacks", "emi.sidebar.type.craft_history": "Crafting History", @@ -60,6 +66,10 @@ "emi.sidebar.type.empty.description": "Displays nothing", "emi.sidebar.type.chess": "Chess", "emi.sidebar.type.chess.description": "Chess", + "emi.tree_bookmark": "Tree Bookmark", + "emi.tree_bookmark.count": "Trees: %s", + "emi.tree_bookmark.name_title": "Name Tree Bookmark", + "emi.tree_bookmark.name_field": "Tree Bookmark Name", "emi.sidebar.theme.transparent": "Transparent", "emi.sidebar.theme.vanilla": "Vanilla", "emi.sidebar.theme.modern": "Modern", @@ -260,11 +270,13 @@ "tooltip.emi.synfav.craft_some": "Press %s to craft as many as possible", "tooltip.emi.synfav.craft_all": "Press %s to craft all §9%s§r batches", - "tooltip.emi.bom.batch_size": "Batch size: %s\nScroll to adjust\nHold §6[shift]§r to adjust by 16", + "tooltip.emi.bom.batch_size": "Batch size: %s\nScroll to adjust\nHold §6[Shift]§r to adjust by 16", "tooltip.emi.bom.batch_size.ideal": "Press %s to get minimal leftovers", + "tooltip.emi.bom.batch_size.multiply": "Hold §6[Ctrl]§r to add multiplier", + "tooltip.emi.bom.batch_size.all": "Hold §6[Alt]§r to apply for all root items", "tooltip.emi.bom.mode.view": "§6Viewing§r recipe tree", "tooltip.emi.bom.mode.craft": "§6Crafting§r recipe tree\nProgress will be shown in recipe tree\n§bSynthetic favorites§r added to sidebar", - "tooltip.emi.bom.help": "The recipe tree shows the process and base cost of a recipe\nThe display can be panned and zoomed\n\nLeft click a node to choose a recipe to assign to it\nHold §6[shift]§r to automatically pick based on your inventory\n\nRight click a recipe node to fold it temporarily\nHold §6[shift]§r to clear the recipe\n\nThe initial tree state is controlled by default recipes\nA button next to recipes can set default recipe preferences\n\nThe toggle by total cost can be used to help craft a recipe tree\nWhile crafting, materials that need to be collected will be displayed\n§bSynthetic favorites§r will be added to the favorites sidebar that\nwill show incomplete steps and can be used to craft", + "tooltip.emi.bom.help": "The recipe tree shows the process and base cost of a recipe\nThe display can be panned and zoomed\n\nLeft click a node to choose a recipe to assign to it\nHold §6[shift]§r to automatically pick based on your inventory\n\nRight click a recipe node to fold it temporarily\nHold §6[shift]§r to clear the recipe\n\nThe initial tree state is controlled by default recipes\nA button next to recipes can set default recipe preferences\n\nThe toggle by total cost can be used to help craft a recipe tree\nWhile crafting, materials that need to be collected will be displayed\n§bSynthetic favorites§r will be added to the favorites sidebar that\nwill show incomplete steps and can be used to craft\n\nPress %s in the recipe tree to save the current tree (including chosen tag variants) as a Tree Bookmark", "tooltip.emi.fluid_interaction.basalt.soul_soil": "Below lava", "tooltip.emi.fluid_interaction.basalt.blue_ice": "Adjacent to lava", diff --git a/xplat/src/main/resources/assets/emi/lang/es_es.json b/xplat/src/main/resources/assets/emi/lang/es_es.json index b5648543..fafe839f 100644 --- a/xplat/src/main/resources/assets/emi/lang/es_es.json +++ b/xplat/src/main/resources/assets/emi/lang/es_es.json @@ -337,6 +337,8 @@ "tooltip.emi.bom.batch_size": "Tamaño del lote: %s\nDesplaza la rueda para ajustar\nMantén §6[shift]§r para ajustar en 16", "tooltip.emi.bom.batch_size.ideal": "Pulsa %s para obtener las mínimas sobras posibles", + "tooltip.emi.bom.batch_size.multiply": "Mantén §6[Ctrl]§r para añadir un multiplicador", + "tooltip.emi.bom.batch_size.all": "Mantén §6[Alt]§r para aplicar a todos los objetos raíz", "tooltip.emi.bom.mode.view": "§6Mostrando§r árbol de recetas", "tooltip.emi.bom.mode.craft": "§6Fabricando§r árbol de recetas\nEl progreso se mostrará en el árbol de recetas\n§bFavoritos sintéticos§r añadidos a la barra lateral", "tooltip.emi.bom.help": "El árbol de recetas muestra la progresión y el coste total de la receta.\nLa vista se puede mover y ampliar.\n\nClick izquierdo en un nodo para seleccionar una receta y asignarla.\nMantén §6[shift]§r para seleccionar automáticamente en función de tu inventario.\n\nClick derecho en un nodo de la receta para plegarlo temporalmente\nMantén §6[shift]§r para vaciar la receta.\n\nEl estado inicial del árbol es controlado por las recetas por defecto.\nUn botón junto a las recetas puede establecer las preferencias por defecto.\n\nEl ajuste por coste total puede usarse para ayudar a crear un árbol.\nMientras esté fabricando, se mostrarán los materiales que hace falta recoger.\nSe añadirán §bFavoritos sintéticos§r a la barra lateral que\nmostrarán los pasos incompletos y podrán usarse para fabricar.", diff --git a/xplat/src/main/resources/assets/emi/lang/fi_fi.json b/xplat/src/main/resources/assets/emi/lang/fi_fi.json index 00b80c46..c443ee82 100644 --- a/xplat/src/main/resources/assets/emi/lang/fi_fi.json +++ b/xplat/src/main/resources/assets/emi/lang/fi_fi.json @@ -226,8 +226,10 @@ "tooltip.emi.synfav.craft_some": "Valmista mahdollisimman monta erää painamalla %s", "tooltip.emi.synfav.craft_all": "Valmista kaikki §9%2$s§r erää painamalla %1$s", - "tooltip.emi.bom.batch_size": "Erän koko: %s\nSäädä vierittämällä\nSäädä 16 kerralla pitämällä pohjassa §6[shift]§r-näppäintä", + "tooltip.emi.bom.batch_size": "Erän koko: %s\nSäädä vierittämällä\nSäädä 16 kerralla pitämällä pohjassa §6[Shift]§r-näppäintä", "tooltip.emi.bom.batch_size.ideal": "Hanki vähäisimmät ylijäämät painamalla %s", + "tooltip.emi.bom.batch_size.multiply": "Pidä §6[Ctrl]§r lisätäksesi kertoimen", + "tooltip.emi.bom.batch_size.all": "Pidä §6[Alt]§r käyttääksesi kaikkiin juurituotteisiin", "tooltip.emi.bom.mode.view": "§6Katsotaan§r reseptipuuta", "tooltip.emi.bom.mode.craft": "§6Valmistetaan§r reseptipuuta\nEdistyminen näkyy reseptipuussa\n§bSynteettiset suosikit§r lisätty sivupalkkiin", "tooltip.emi.bom.help": "Reseptipuu näyttää reseptin prosessin ja pohjakulut\nNäkymää voi siirrellä ja suurentaa\n\nAseta solmukohdan resepti hiiren vasemmalla painikkeella\nValitse automaattisesti tavaraluettelon perusteella §6[shift]§r-näppäimellä\n\nPienennä reseptisolmu väliaikaisesti hiiren oikealla painikkeella\nTyhjennä resepti §6[shift]§r-näppäimellä\n\nPuun lähtötilaa ohjataan oletusresepteillä\nOletusreseptit voi asettaa reseptien viereisellä painikkeella\n\nReseptipuun valmistamisessa voi auttaa kokonaiskulujen kytkeminen\nValmistuksen aikana näytetään materiaalit, jotka täytyy kerätä\nSuosikkipalkkiin lisätään §bsynteettisiä suosikkeja§r, jotka näyttävät\nkeskeneräiset vaiheet ja joita voi käyttää valmistamiseen", diff --git a/xplat/src/main/resources/assets/emi/lang/fr_fr.json b/xplat/src/main/resources/assets/emi/lang/fr_fr.json index 03c31c74..72781bc5 100644 --- a/xplat/src/main/resources/assets/emi/lang/fr_fr.json +++ b/xplat/src/main/resources/assets/emi/lang/fr_fr.json @@ -316,7 +316,9 @@ "tooltip.emi.synfav.partially_craftable": "§dImpossible§r de tout faire\n§6Cliquez§r pour en fabriquer", "tooltip.emi.synfav.fully_craftable": "§aPossible§r de tout faire §a(%1$s)§r\n§6Cliquez§r pour en fabriquer", - "tooltip.emi.bom.batch_size": "Quantité: %s\nScrollez pour changer\nMaintenez §6[shift]§r pour changer par 16", + "tooltip.emi.bom.batch_size": "Quantité: %s\nScrollez pour changer\nMaintenez §6[Shift]§r pour changer par 16", + "tooltip.emi.bom.batch_size.multiply": "Maintenez §6[Ctrl]§r pour ajouter un multiplicateur", + "tooltip.emi.bom.batch_size.all": "Maintenez §6[Alt]§r pour appliquer à tous les objets racine", "tooltip.emi.bom.mode.view": "§6Assistant de fabrication§r\n(Désactivé)\n\nClic gauche pour l'activer", "tooltip.emi.bom.mode.craft": "§6Assistant de fabrication§r\nVotre progression sera\nvisible dans cette liste, des \n§bfavoris temporaires§r seront\najoutés pour vous guider", "tooltip.emi.bom.help": "Le graphe liste les ingrédients et les recettes\nVous pouvez déplacer/zoomer ce graphe\n\nClic gauche sur un noeud permet de modifier une recette\nMaintenir §6[Maj]§r permet de choisir automatiquement une recette\n\nClic droit un un noeud permet de le masquer temporairement\nMaintenir §6[Maj]§r permet de supprimer le noeud\n\nLe graphe est généré selon vos recettes par défaut\n\nLe bouton \"§acoeur§r\" permet de mettre des recettes en favoris\nSi vous utilisez l'§6assistant de fabrication§r alors\n\ndes §bfavoris temporaires§r seront placés dans l'onglet\ndes favoris afin de vous guider dans votre fabrication", diff --git a/xplat/src/main/resources/assets/emi/lang/ja_jp.json b/xplat/src/main/resources/assets/emi/lang/ja_jp.json index 8362a78e..8849196e 100644 --- a/xplat/src/main/resources/assets/emi/lang/ja_jp.json +++ b/xplat/src/main/resources/assets/emi/lang/ja_jp.json @@ -336,6 +336,8 @@ "tooltip.emi.bom.batch_size": "一度にまとめてクラフトする数: %s\nスクロールして調整します\n§6[Shift]§r を押したままにして16ずつ調整します", "tooltip.emi.bom.batch_size.ideal": "%s を押して残り物を最小限にする", + "tooltip.emi.bom.batch_size.multiply": "§6[Ctrl]§r を押しながらで倍率を追加", + "tooltip.emi.bom.batch_size.all": "§6[Alt]§r を押しながらで全てのルート項目に適用", "tooltip.emi.bom.mode.view": "§6閲覧中§r レシピツリー", "tooltip.emi.bom.mode.craft": "§6クラフト§rレシピツリー\n進捗をレシピツリー\n§b合成お気に入り§rをサイドバーに追加しました", "tooltip.emi.bom.help": "レシピ ツリーには, レシピのプロセスとコストが表示されます。\n表示は上下左右へのドラッグや拡大縮小が可能です。\n\nノードを左クリックして, それに割り当てるレシピを選択します。\n§6[Shift]§r を押し続けると, 在庫に基づいて自動的に選択されます。\n\nレシピノードを右クリックして一時的に折りたたむことができ,\n§6[Shift]§r を押し続けるとレシピをクリアできます。\n\nツリーの初期状態はデフォルトのレシピに従います。\nレシピの横にあるボタンでデフォルトのレシピ設定を設定できます。\n\n総コストによる切り替えは, レシピ ツリーの作成に役立ちます。\n製作中は収集が必要な素材が表示されます。\n§b合成お気に入り§r は, お気に入りサイドバーに追加されます。\n不完全な手順が表示され, クラフトに使用できます。", diff --git a/xplat/src/main/resources/assets/emi/lang/pt_br.json b/xplat/src/main/resources/assets/emi/lang/pt_br.json index 9f2f2280..4db7f773 100644 --- a/xplat/src/main/resources/assets/emi/lang/pt_br.json +++ b/xplat/src/main/resources/assets/emi/lang/pt_br.json @@ -339,8 +339,10 @@ "tooltip.emi.synfav.craft_some": "Pressione %s para fabricar o máximo possível", "tooltip.emi.synfav.craft_all": "Pressione %s para fabricar todos os §9%s§r lotes", - "tooltip.emi.bom.batch_size": "Tamanho do lote: %s\nRole para ajustar\nSegure §6[shift]§r para ajustar em 16", + "tooltip.emi.bom.batch_size": "Tamanho do lote: %s\nRole para ajustar\nSegure §6[Shift]§r para ajustar em 16", "tooltip.emi.bom.batch_size.ideal": "Pressione %s para minimizar sobras", + "tooltip.emi.bom.batch_size.multiply": "Segure §6[Ctrl]§r para adicionar multiplicador", + "tooltip.emi.bom.batch_size.all": "Segure §6[Alt]§r para aplicar a todos os itens raiz", "tooltip.emi.bom.mode.view": "§6Visualizando§r árvore de receitas", "tooltip.emi.bom.mode.craft": "§6Criando§r árvore de receitas\nO progresso será mostrado na árvore de receitas\n§bFavoritos sintéticos§r adicionados à barra lateral", "tooltip.emi.bom.help": "A Árvore de Receitas mostra o processo e o custo base de criação de uma receita.\nA exibição da Árvore pode ser movida e ampliada.\n\nClique com o botão esquerdo em um nó para atribuir uma receita a ele.\nSegure §6[shift]§r para escolher automaticamente com base no seu inventário.\n\nClique com o botão direito em um nó de receita para o minimizar temporariamente.\nSegure §6[shift]§r para ocultar a receita.\n\nO estado inicial da árvore é controlado por receitas padrão.\nUse o botão ao lado das receitas para definir preferências de receita padrão.\n\nA mudança por custo total pode ser utilizada para ajudar a criar uma árvore de receitas.\nDurante a fabricação, os materiais que precisam ser coletados serão exibidos.\n§bFavoritos sintéticos§r serão adicionados à barra lateral de favoritos,\nmostrando etapas incompletas e podendo ser usados para fabricar.", diff --git a/xplat/src/main/resources/assets/emi/lang/ru_ru.json b/xplat/src/main/resources/assets/emi/lang/ru_ru.json index f7ecb2d2..4c76539d 100644 --- a/xplat/src/main/resources/assets/emi/lang/ru_ru.json +++ b/xplat/src/main/resources/assets/emi/lang/ru_ru.json @@ -32,6 +32,8 @@ "key.emi.copy_recipe_id": "Скопировать ID рецепта", "key.emi.hide_stack": "Скрыть предмет", "key.emi.hide_stack_by_id": "Скрыть предмет по ID", + "key.emi.add_tree_bookmark": "Добавить закладку дерева", + "key.emi.rename_tree_bookmark": "Переименовать закладку дерева", "emi.index_source.creative": "Творческие вкладки", "emi.index_source.registered": "Зарегистрированные", @@ -60,6 +62,12 @@ "emi.sidebar.type.empty.description": "Ничего не отображает", "emi.sidebar.type.chess": "Шахматы", "emi.sidebar.type.chess.description": "Шахматы", + "emi.sidebar.type.tree_bookmarks": "Закладки дерева", + "emi.sidebar.type.tree_bookmarks.description": "Показывает сохранённые деревья рецептов", + "emi.tree_bookmark": "Закладка дерева", + "emi.tree_bookmark.count": "Деревьев: %s", + "emi.tree_bookmark.name_title": "Имя закладки дерева", + "emi.tree_bookmark.name_field": "Название закладки дерева", "emi.sidebar.theme.transparent": "Прозрачная", "emi.sidebar.theme.vanilla": "Ванильная", "emi.sidebar.theme.modern": "Современная", @@ -262,9 +270,11 @@ "tooltip.emi.bom.batch_size": "Размер пакета: %s\nПрокрутите, чтобы отрегулировать\nЗажмите §6[shift]§r, чтобы изменять по 16", "tooltip.emi.bom.batch_size.ideal": "Нажмите %s, чтобы получить меньше всего остатков", + "tooltip.emi.bom.batch_size.multiply": "Зажмите §6[Ctrl]§r, чтобы добавить множитель", + "tooltip.emi.bom.batch_size.all": "Зажмите §6[Alt]§r, чтобы применить ко всем корневым предметам", "tooltip.emi.bom.mode.view": "§6Просматриваете§r дерево рецептов", "tooltip.emi.bom.mode.craft": "Дерево рецептов §6создания§r\nПрогресс будет показан в дереве рецептов\n§bSynthetic favorites§r added to sidebar", - "tooltip.emi.bom.help": "Дерево рецептов показывает прогресс и базовую стоимость рецепта\nЭкран можно двигать и приближать\n\nНажмите ЛКМ по узлу, чтобы выбрать рецепт, который будет использоваться\nЗажмите §6[shift]§r, чтобы автоматически выбрать, основываясь на вашем инвентаре\n\nНажмите ПКМ по узлу рецептов, чтобы временно его свернуть\nЗажмите §6[shift]§r, чтобы убрать из рецепта\n\nНачальное состояние дерева контролируется рецептами по умолчанию\nКнопка рядом с рецептами может установить предпочтительный стандартный рецепт\n\nПереключатель рядом с общей стоимостью может помочь в создании дерева рецептов\nПри создании дерева материалы, которые ещё предстоит собрать, будут отображены\n\n§bИскусственно избранные предметы§r будут добавлены в боковую панель с избранными предметами.\nОни будут показывать ещё незавершённые шаги к созданию нужного предмета в дереве", + "tooltip.emi.bom.help": "Дерево рецептов показывает прогресс и базовую стоимость рецепта\nЭкран можно двигать и приближать\n\nНажмите ЛКМ по узлу, чтобы выбрать рецепт, который будет использоваться\nЗажмите §6[shift]§r, чтобы автоматически выбрать, основываясь на вашем инвентаре\n\nНажмите ПКМ по узлу рецептов, чтобы временно его свернуть\nЗажмите §6[shift]§r, чтобы убрать из рецепта\n\nНачальное состояние дерева контролируется рецептами по умолчанию\nКнопка рядом с рецептами может установить предпочтительный стандартный рецепт\n\nПереключатель рядом с общей стоимостью может помочь в создании дерева рецептов\nПри создании дерева материалы, которые ещё предстоит собрать, будут отображены\n\n§bИскусственно избранные предметы§r будут добавлены в боковую панель с избранными предметами.\nОни будут показывать ещё незавершённые шаги к созданию нужного предмета в дереве\n\nНажмите %s в дереве рецептов, чтобы сохранить текущее дерево (включая выбранные варианты тегов) как закладку дерева", "tooltip.emi.fluid_interaction.basalt.soul_soil": "Под лавой", "tooltip.emi.fluid_interaction.basalt.blue_ice": "На уровне с лавой", diff --git a/xplat/src/main/resources/assets/emi/lang/tr_tr.json b/xplat/src/main/resources/assets/emi/lang/tr_tr.json index b878d288..0cc7b2c8 100644 --- a/xplat/src/main/resources/assets/emi/lang/tr_tr.json +++ b/xplat/src/main/resources/assets/emi/lang/tr_tr.json @@ -400,5 +400,10 @@ "tag.fabric.shovels": "Kürekler", "tag.fabric.hoes": "Çapalar", "tag.fabric.swords": "Kılıçlar", - "tag.fabric.shears": "Makaslar" + "tag.fabric.shears": "Makaslar", + + "tooltip.emi.bom.batch_size": "Üretim miktarı: %s\nAyarlamak için kaydırın\n16'şar ayarlamak için §6[Shift]§r tuşunu basılı tutun", + "tooltip.emi.bom.batch_size.ideal": "Artıkları en aza indirmek için %s tuşuna basın", + "tooltip.emi.bom.batch_size.multiply": "Çarpan eklemek için §6[Ctrl]§r tuşunu basılı tutun", + "tooltip.emi.bom.batch_size.all": "Tüm kök öğelere uygulamak için §6[Alt]§r tuşunu basılı tutun" } diff --git a/xplat/src/main/resources/assets/emi/lang/zh_cn.json b/xplat/src/main/resources/assets/emi/lang/zh_cn.json index b07b4ed9..abeef203 100644 --- a/xplat/src/main/resources/assets/emi/lang/zh_cn.json +++ b/xplat/src/main/resources/assets/emi/lang/zh_cn.json @@ -353,6 +353,8 @@ "tooltip.emi.bom.batch_size": "产物份数: %s\n滚动滚轮调整份数\n按住 §6[Shift]§r 滚动会增减16", "tooltip.emi.bom.batch_size.ideal": "按 %s 将副产物数量降到最低", + "tooltip.emi.bom.batch_size.multiply": "按住 §6[Ctrl]§r 添加倍数", + "tooltip.emi.bom.batch_size.all": "按住 §6[Alt]§r 将调整应用到所有根物品", "tooltip.emi.bom.mode.view": "正在§6查看§r配方树", "tooltip.emi.bom.mode.craft": "正在§6制作§r配方树\n配方树中会显示制作进度\n§b收藏夹§r中可查看并快捷合成", "tooltip.emi.bom.help": "欢迎使用配方树\n这里可以分析物品的制作过程和原材料消耗\n按 §6[Ctrl + R]§r 可展示随机配方树\n本界面可以拖动或缩放\n\n配方树会自动使用默认配方分析制作过程\n配方详情页中可以更改默认配方\n\n左键物品/节点可手动选择配方\n按住 §6[Shift]§r 左键自动选择配方(根据物品栏)\n\n右键节点可以暂时折叠配方\n按住 §6[Shift]§r 右键节点可以清除配方\n\n选中最终产物旁的数可以改变产物份数\n\n总耗材旁的按钮可切换至§6制作§r模式\n在§6制作§r模式下,§b收藏夹§r中会显示需要收集的材料\n和中间产物,这些中间产物可以点击以快速合成", diff --git a/xplat/src/main/resources/assets/emi/textures/gui/buttons.png b/xplat/src/main/resources/assets/emi/textures/gui/buttons.png index 2bf4fa56..633951f1 100644 Binary files a/xplat/src/main/resources/assets/emi/textures/gui/buttons.png and b/xplat/src/main/resources/assets/emi/textures/gui/buttons.png differ