Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dc7eab3
feat: Button for share recipe action
DanielKnourek Oct 25, 2025
9543de9
feat: Server command to send recipe to whole server
DanielKnourek Oct 26, 2025
dea7bfe
refactor: move share message to client
DanielKnourek Oct 30, 2025
90a6825
fix: hide RecipeShareButtonWidget for unsupported handlers
DanielKnourek Oct 30, 2025
2d8279d
feat: add text to translatable
DanielKnourek Oct 30, 2025
7d362b9
fix: translatable path
DanielKnourek Oct 30, 2025
f0570b7
feat: add keybind for EmiShareRecipe
DanielKnourek Oct 30, 2025
a586383
refactor: moving EmiShareRecipe to runtime package
DanielKnourek Oct 30, 2025
7f58bd6
fix: incorrect translatable string
DanielKnourek Oct 30, 2025
ca1f976
fix: linter problems
DanielKnourek Oct 30, 2025
019bb9e
fix: do not enforce being visible by default
DanielKnourek Nov 8, 2025
b12f78a
feat: add sharing history sidebar
DanielKnourek Nov 10, 2025
385b7a5
fix: empty panel not being last
DanielKnourek Nov 10, 2025
66a3eab
fix: adding missing panel icon
DanielKnourek Nov 10, 2025
9b244eb
Merge pull request #2 from DanielKnourek/feature/EMIShareRecipe
DanielKnourek Nov 10, 2025
4493511
Fix: compatibility issues between versions
DanielKnourek Nov 10, 2025
ccf4394
feat: add limit to shareHistory
DanielKnourek Nov 10, 2025
9d5e490
feat: add fallback option for non-standard recipe handlers
DanielKnourek Nov 11, 2025
710c606
refactor: bring sharing logic to single place
DanielKnourek Nov 11, 2025
6d072ff
feat: option to disable chat share message
DanielKnourek Nov 27, 2025
12336bb
fix: screenshot config entry reorder
DanielKnourek Nov 27, 2025
5b7458b
fix: missing button locale entry
DanielKnourek Nov 27, 2025
26162f9
Merge branch 'emilyploszaj:1.21' into 1.21
DanielKnourek Nov 28, 2025
e16cd2a
refactor: spaces to tabs
DanielKnourek Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions xplat/src/main/java/dev/emi/emi/api/widget/SlotWidget.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import dev.emi.emi.runtime.EmiDrawContext;
import dev.emi.emi.runtime.EmiFavorites;
import dev.emi.emi.runtime.EmiHistory;
import dev.emi.emi.runtime.EmiShareRecipe;
import dev.emi.emi.screen.EmiScreenManager;
import dev.emi.emi.screen.RecipeScreen;
import dev.emi.emi.screen.tooltip.EmiTooltip;
Expand Down Expand Up @@ -247,6 +248,9 @@ protected void addSlotTooltip(List<TooltipComponent> list) {
} else if (EmiConfig.favorite.isBound() && EmiConfig.helpLevel.has(HelpLevel.NORMAL) && EmiFavorites.canFavorite(getStack(), getRecipe())) {
list.add(TooltipComponent.of(EmiPort.ordered(EmiPort.translatable("emi.favorite_recipe", EmiConfig.favorite.getBindText()))));
}
if (EmiConfig.share.isBound() && EmiConfig.helpLevel.has(HelpLevel.NORMAL) && EmiShareRecipe.isSupportedRecipe(recipe)){
list.add(TooltipComponent.of(EmiPort.ordered(EmiPort.translatable("emi.resolve.share", EmiConfig.share.getBindText()))));
}
if (EmiConfig.showCostPerBatch && recipe.supportsRecipeTree() && !(recipe instanceof EmiResolutionRecipe)) {
RecipeCostTooltipComponent rctc = new RecipeCostTooltipComponent(recipe);
if (rctc.shouldDisplay()) {
Expand Down
18 changes: 15 additions & 3 deletions xplat/src/main/java/dev/emi/emi/config/EmiConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ public class EmiConfig {
@ConfigValue("ui.recipe-tree-button-visibility")
public static ButtonVisibility recipeTreeButtonVisibility = ButtonVisibility.AUTO;

@Comment("Whether the shared recipe should be visible in chat.")
@ConfigValue("ui.recipe-share-chat-message-visibility")
public static boolean recipeShareChatMessageVisibility = true;

@ConfigGroup("ui.recipe-screen")
@Comment("The maximum height the recipe screen will grow to be if space is available in pixels.")
@ConfigValue("ui.maximum-recipe-screen-height")
Expand Down Expand Up @@ -184,9 +188,13 @@ public class EmiConfig {
@ConfigValue("ui.recipe-fill-button")
public static boolean recipeFillButton = true;

@Comment("Whether recipes should have a button to take a screenshot of the recipe.")
@ConfigValue("ui.recipe-screenshot-button")
public static boolean recipeScreenshotButton = false;
@Comment("Whether recipes should have a button to share current recipe to other players.")
@ConfigValue("ui.recipe-share-button")
public static boolean recipeShareButton = false;

@Comment("Whether recipes should have a button to take a screenshot of the recipe.")
@ConfigValue("ui.recipe-screenshot-button")
public static boolean recipeScreenshotButton = false;

@ConfigGroupEnd
@Comment("The GUI scale at which recipe screenshots are saved. Use 0 to use the current GUI scale.")
Expand Down Expand Up @@ -378,6 +386,10 @@ public class EmiConfig {
@ConfigValue("binds.favorite")
public static EmiBind favorite = new EmiBind("key.emi.favorite", GLFW.GLFW_KEY_A);

@Comment("Share the recipe in chat to other players for quick access.")
@ConfigValue("binds.share")
public static EmiBind share = new EmiBind("key.emi.share", GLFW.GLFW_KEY_T);

@Comment("Set the default recipe for a given stack in the output of a recipe to that recipe.")
@ConfigValue("binds.default-stack")
public static EmiBind defaultStack = new EmiBind("key.emi.default_stack",
Expand Down
1 change: 1 addition & 0 deletions xplat/src/main/java/dev/emi/emi/config/SidebarType.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum SidebarType implements ConfigEnum {
FAVORITES("favorites", 32, 146),
LOOKUP_HISTORY("lookup-history", 80, 146),
CRAFT_HISTORY("craft-history", 64, 146),
SHARE_HISTORY("share", 112, 146),
EMPTY("empty", 96, 146),
CHESS("chess", 48, 146),
;
Expand Down
82 changes: 50 additions & 32 deletions xplat/src/main/java/dev/emi/emi/network/CommandS2CPacket.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,38 @@
import dev.emi.emi.api.stack.EmiStack;
import dev.emi.emi.bom.BoM;
import dev.emi.emi.registry.EmiCommands;
import dev.emi.emi.runtime.EmiShareRecipe;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.util.Identifier;

public class CommandS2CPacket implements EmiPacket {
private final byte type;
private final Identifier id;
private final byte type;
private final Identifier id;
private String extraInfo;

public CommandS2CPacket(byte type, Identifier id) {
this.type = type;
this.id = id;
}
public CommandS2CPacket(byte type, Identifier id) {
this.type = type;
this.id = id;
}

public CommandS2CPacket(byte type, Identifier id, String extraInfo){
this.type = type;
this.id = id;
this.extraInfo = extraInfo;

}

public CommandS2CPacket(PacketByteBuf buf) {
type = buf.readByte();
if (type == EmiCommands.VIEW_RECIPE || type == EmiCommands.TREE_GOAL || type == EmiCommands.TREE_RESOLUTION) {
id = buf.readIdentifier();
} else {
} else if (type == EmiCommands.SHARE_RECIPE) {
id = buf.readIdentifier();
extraInfo = buf.readString();

} else {
id = null;
}
}
Expand All @@ -33,32 +46,37 @@ public void write(RegistryByteBuf buf) {
buf.writeByte(type);
if (type == EmiCommands.VIEW_RECIPE || type == EmiCommands.TREE_GOAL || type == EmiCommands.TREE_RESOLUTION) {
buf.writeIdentifier(id);
}
}
} else if (type == EmiCommands.SHARE_RECIPE) {
buf.writeIdentifier(id);
buf.writeString(this.extraInfo);
}
}

@Override
public void apply(PlayerEntity player) {
if (type == EmiCommands.VIEW_RECIPE) {
EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe != null) {
EmiApi.displayRecipe(recipe);
}
} else if (type == EmiCommands.VIEW_TREE) {
EmiApi.viewRecipeTree();
} else if (type == EmiCommands.TREE_GOAL) {
EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe != null) {
BoM.setGoal(recipe);
}
} else if (type == EmiCommands.TREE_RESOLUTION) {
EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe != null && BoM.tree != null) {
for (EmiStack stack : recipe.getOutputs()) {
BoM.tree.addResolution(stack, recipe);
}
}
}
}
@Override
public void apply(PlayerEntity player) {
if (type == EmiCommands.VIEW_RECIPE) {
EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe != null) {
EmiApi.displayRecipe(recipe);
}
} else if (type == EmiCommands.VIEW_TREE) {
EmiApi.viewRecipeTree();
} else if (type == EmiCommands.TREE_GOAL) {
EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe != null) {
BoM.setGoal(recipe);
}
} else if (type == EmiCommands.TREE_RESOLUTION) {
EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe != null && BoM.tree != null) {
for (EmiStack stack : recipe.getOutputs()) {
BoM.tree.addResolution(stack, recipe);
}
}
} else if (type == EmiCommands.SHARE_RECIPE) {
EmiShareRecipe.receiveMessage(player, id, extraInfo);
}
}

@Override
public Id<CommandS2CPacket> getId() {
Expand Down
47 changes: 35 additions & 12 deletions xplat/src/main/java/dev/emi/emi/registry/EmiCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
import net.minecraft.util.Identifier;

public class EmiCommands {
public static final byte VIEW_RECIPE = 0x01;
public static final byte VIEW_TREE = 0x02;
public static final byte TREE_GOAL = 0x11;
public static final byte TREE_RESOLUTION = 0x12;

public static void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher) {
public static final byte VIEW_RECIPE = 0x01;
public static final byte VIEW_TREE = 0x02;
public static final byte SHARE_RECIPE = 0x4;
public static final byte TREE_GOAL = 0x11;
public static final byte TREE_RESOLUTION = 0x12;

public static void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(literal("emi")
.requires(source -> source.hasPermissionLevel(2))
.requires(source -> source.hasPermissionLevel(0))
.then(
literal("view")
.then(
Expand All @@ -46,6 +47,7 @@ public static void registerCommands(CommandDispatcher<ServerCommandSource> dispa
)
.then(
literal("tree")
.requires(source -> source.hasPermissionLevel(2))
.then(
literal("goal")
.then(
Expand All @@ -67,10 +69,31 @@ public static void registerCommands(CommandDispatcher<ServerCommandSource> dispa
)
)
)
);
}
.then(
literal("share")
.then(
literal("recipe")
.then(
argument("id", identifier())
.executes(context -> {
Identifier id = context.getArgument("id", Identifier.class);

// TODO: resolve, if command is executed by server, its does not work, but other commands are not supported either
context.getSource().getServer().getPlayerManager().getPlayerList().forEach(player -> {
EmiNetwork.sendToClient(
player,
new CommandS2CPacket(SHARE_RECIPE, id, context.getSource().getPlayer().getName().getString())
);
});
return Command.SINGLE_SUCCESS;
})
)
)
)
);
}

private static void send(ServerPlayerEntity player, byte type, @Nullable Identifier id) {
EmiNetwork.sendToClient(player, new CommandS2CPacket(type, id));
}
private static void send(ServerPlayerEntity player, byte type, @Nullable Identifier id) {
EmiNetwork.sendToClient(player, new CommandS2CPacket(type, id));
}
}
107 changes: 107 additions & 0 deletions xplat/src/main/java/dev/emi/emi/runtime/EmiShareRecipe.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package dev.emi.emi.runtime;

import com.google.common.collect.Lists;
import dev.emi.emi.api.EmiApi;
import dev.emi.emi.api.recipe.EmiRecipe;
import dev.emi.emi.config.EmiConfig;
import dev.emi.emi.config.SidebarType;
import dev.emi.emi.screen.EmiScreenManager;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.text.ClickEvent;
import net.minecraft.text.HoverEvent;
import net.minecraft.text.MutableText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;

import java.util.List;
import java.util.Objects;

public class EmiShareRecipe {

private static int HISTORY_SIZE = 32;
public static List<EmiFavorite> shareHistory = Lists.newArrayList();
private static MinecraftClient client = MinecraftClient.getInstance();

public static void receiveMessage(PlayerEntity player, Identifier id, String senderDisplayName) {

EmiRecipe recipe = EmiApi.getRecipeManager().getRecipe(id);
if (recipe == null) {
EmiLog.error("Could not create sharing message. Could not find recipe.");
return;
}

EmiFavorite sharedRecipe;

if(!recipe.getOutputs().isEmpty() && !recipe.getOutputs().get(0).isEmpty()) {
sharedRecipe = new EmiFavorite(recipe.getOutputs().get(0).getEmiStacks().get(0), recipe);
} else if (!recipe.getInputs().isEmpty()) {
// Non result recipe, using ingredient
sharedRecipe = new EmiFavorite(recipe.getInputs().get(0), recipe);
} else {
EmiLog.error("Could not create sharing message. Invalid Recipe.");
return;
}

// Check if you are adding the same recipe to the history again, if yes, stop.
if(!shareHistory.isEmpty() && recipe.getId().equals(shareHistory.get(0).getRecipe().getId())){
// Command is received 2x on a client, allow only first occurrence of this command
// also works as primitive spam protection
//TODO: find out why it is triggered 2 times
return;
}
shareHistory.removeIf(e -> Objects.equals(e.getRecipe().getId(), sharedRecipe.getRecipe().getId()));
shareHistory.add(0,sharedRecipe);
if(shareHistory.size() > HISTORY_SIZE) {
shareHistory.subList(HISTORY_SIZE, shareHistory.size()).clear();
}
EmiScreenManager.repopulatePanels(SidebarType.SHARE_HISTORY);

if(EmiConfig.recipeShareChatMessageVisibility){
String itemDisplayName = "View Recipe"; // fallback display text
if (!sharedRecipe.getStack().getEmiStacks().isEmpty()){
itemDisplayName = sharedRecipe.getStack().getEmiStacks().get(0).getItemStack().getItem().getName().getString();
}
MutableText clickableId = Text.literal(String.format("[%s]", itemDisplayName));

Style style = Style.EMPTY
.withColor(Formatting.UNDERLINE)
.withColor(Formatting.AQUA)
.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/emi view recipe " + id))
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("tooltip.emi.recipe_share_chat")));
clickableId.setStyle(style);

Text message = Text.translatable("chat.emi.recipe_share", senderDisplayName, clickableId);

player.sendMessage(message, false);
}
}

public static boolean sendMessage(EmiRecipe recipe){
if(!isSupportedRecipe(recipe)){
EmiLog.error("Unable to create recipe for [" + recipe + "]. Recipe handler not supported");
return false;
}

Identifier id = recipe.getId();

if (client.player == null) {
return false;
}

String shareCommand = String.format("emi share recipe %s", id);
client.player.networkHandler.sendChatCommand(shareCommand);
return true;
}

public static boolean isSupportedRecipe(EmiRecipe recipe){
if (recipe != null && recipe.getId() != null) {
return true;
}

EmiLog.error("Unable to create recipe for [" + recipe + "]. Recipe handler not supported");
return false;
}
}
1 change: 1 addition & 0 deletions xplat/src/main/java/dev/emi/emi/runtime/EmiSidebars.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static List<? extends EmiIngredient> getStacks(SidebarType type) {
case FAVORITES -> EmiFavorites.favoriteSidebar;
case LOOKUP_HISTORY -> lookupHistory;
case CRAFT_HISTORY -> craftHistory;
case SHARE_HISTORY -> EmiShareRecipe.shareHistory;
case EMPTY -> List.of();
case CHESS -> EmiChess.SIDEBAR;
default -> List.of();
Expand Down
7 changes: 5 additions & 2 deletions xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import net.minecraft.command.argument.ItemStackArgument;
import net.minecraft.component.ComponentChanges;
import dev.emi.emi.runtime.EmiShareRecipe;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4fStack;
import org.lwjgl.glfw.GLFW;
Expand Down Expand Up @@ -1249,8 +1250,10 @@ public static boolean stackInteraction(EmiStackInteraction stack, Function<EmiBi
BoM.setGoal(stack.getRecipeContext());
EmiApi.viewRecipeTree();
return true;
}
Supplier<EmiRecipe> supplier = () -> {
} else if (function.apply(EmiConfig.share) && stack.getRecipeContext() != null) {
return EmiShareRecipe.sendMessage(stack.getRecipeContext());
}
Supplier<EmiRecipe> supplier = () -> {
return EmiUtil.getPreferredRecipe(ingredient, lastPlayerInventory, true);
};
if (craftInteraction(ingredient, supplier, stack, function)) {
Expand Down
Loading