From 1a61fb2ca013ee6854e77bf44681b62dd4b4157f Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 22:06:50 -0700 Subject: [PATCH 1/6] chore: rename license file --- LICENSE.txt => LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename LICENSE.txt => LICENSE.md (97%) diff --git a/LICENSE.txt b/LICENSE.md similarity index 97% rename from LICENSE.txt rename to LICENSE.md index 9425307..b53b7ea 100644 --- a/LICENSE.txt +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2023 SweetRPG +Copyright © 2023-6 SweetRPG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the From 89315cbed33fd5bdd358b238a009759f4f962f9d Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 22:07:40 -0700 Subject: [PATCH 2/6] chore: update spec to add design and tasks --- .../open-recipe-in-item-viewer/design.md | 135 ++++++++++++++++++ .../open-recipe-in-item-viewer/tasks.md | 10 ++ 2 files changed, 145 insertions(+) create mode 100644 openspec/changes/open-recipe-in-item-viewer/design.md create mode 100644 openspec/changes/open-recipe-in-item-viewer/tasks.md diff --git a/openspec/changes/open-recipe-in-item-viewer/design.md b/openspec/changes/open-recipe-in-item-viewer/design.md new file mode 100644 index 0000000..2979f8e --- /dev/null +++ b/openspec/changes/open-recipe-in-item-viewer/design.md @@ -0,0 +1,135 @@ +# Design: open-recipe-in-item-viewer + +## Overview + +When a player hovers an entry in the Craft Queue HUD overlay and presses a configurable key, +Craft Tracker calls the active item-viewer integration to show recipes for that item. The action +is routed through the `Addon` abstraction so JEI API code stays quarantined in `integration/jei/` +and the feature is silently inert when no supported viewer is installed. + +## Components + +### 1. `Addon` interface — new capability method + +Add a default no-op method: + +```java +default void showRecipesFor(ItemStack stack) { +} +``` + +Addons that support recipe display override this method. The no-op default ensures unknown or +non-viewer addons compile and run without change. + +### 2. `JeiAddon` (new) — `integration/jei/JeiAddon.java` + +Implements `Addon`. Responsible for the JEI-specific recipe-display call. + +- `getName()` → `"jei"` +- `getMods()` → `List.of("jei")` +- `showRecipesFor(ItemStack stack)`: + +```java +IJeiRuntime runtime = CTPlugin.jeiRuntime; +if(runtime ==null)return; +IFocus focus = runtime.getJeiHelpers() + .getFocusFactory() + .createFocus(IRecipeCategory.RecipeIngredientRole.OUTPUT, VanillaTypes.ITEM_STACK, stack); +runtime. + +getRecipesGui(). + +showTypes(List.of(focus)); +``` + +`CTPlugin.jeiRuntime` is already a `public static` field set by `onRuntimeAvailable`, so +`JeiAddon` can read it without a back-reference to `CTPlugin`. + +### 3. `AddonManager` — register `JeiAddon` + dispatch method + +- Add `new JeiAddon()` to `ADDONS`. +- Add: + +```java +public static void showRecipesFor(ItemStack stack) { + doWork(RUN, Addon::shouldLoad, + addon -> addon.showRecipesFor(stack), + (addon, e) -> CraftTracker.LOGGER.warn("Failed to show recipes via {}", addon.getName())); +} +``` + +`doWork` already swallows `RuntimeException` per addon, satisfying the spec requirement that a +failure must not propagate as an unhandled exception. + +### 4. `CraftQueueOverlay` — hover tracking + +Add a package-visible static field: + +```java +static ItemStack hoveredItem = null; +``` + +During the render lambda, after computing each row's `yPos`, compare the raw mouse cursor +position (via `Minecraft.getInstance().mouseHandler`) against the item icon bounding box +`(x + SECTION_X_OFFSET, yPos, 16, 16)` for every product and intermediate row. Set +`hoveredItem` to the matching `ItemStack`, or `null` if none match. + +Mouse coordinates from `mouseHandler` are in physical pixels; divide by +`mc.getWindow().getGuiScale()` to get GUI coordinates before comparing. + +### 5. `Constants` — new translation key + +```java +public static final String TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE = "key.crafttracker.showRecipe"; +``` + +### 6. `ModKeyBindings` — new key binding + +```java +public static final KeyMapping SHOW_RECIPE_MAPPING = new KeyMapping( + Constants.TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE, + KeyConflictContext.GUI, + InputConstants.Type.KEYSYM, + InputConstants.KEY_R, + Constants.KEY_BINDINGS_CATEGORY_TITLE +); +``` + +Register it in `init()` with `ClientRegistry.registerKeyBinding(SHOW_RECIPE_MAPPING)`. + +### 7. `ClientEventHandler.onKeyInput` — dispatch + +In the `screen != null` branch, add a check after the existing `ADD_TO_QUEUE_MAPPING` block: + +```java +if(ModKeyBindings.SHOW_RECIPE_MAPPING.matches(event.getKey(),event. + +getScanCode())){ +ItemStack hovered = CraftQueueOverlay.hoveredItem; + if(hovered !=null){ + AddonManager. + +showRecipesFor(hovered); + } + } +``` + +No screen-type guard needed — `hoveredItem` is only non-null when the mouse is actually over +a CT overlay entry, which implicitly gates the action. + +### 8. `CTLangProvider` — English, en-GB, German strings + +Add to all three locale methods: + +```java +add(Constants.TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE, "Show Recipe"); +``` + +Run `./gradlew data` after to regenerate the committed JSON lang files. + +## What is NOT in scope + +- Shopping list overlay hover (raw materials, fuel) — items there have no crafting recipe to + show in an interesting way; can be added in a follow-up. +- REI integration — deferred to a future addon implementation. +- Click (mouse button) as the trigger — keyboard-only for now, consistent with existing bindings. diff --git a/openspec/changes/open-recipe-in-item-viewer/tasks.md b/openspec/changes/open-recipe-in-item-viewer/tasks.md new file mode 100644 index 0000000..f69aec5 --- /dev/null +++ b/openspec/changes/open-recipe-in-item-viewer/tasks.md @@ -0,0 +1,10 @@ +# Tasks: open-recipe-in-item-viewer + +- [x] Add `showRecipesFor(ItemStack)` default no-op to `Addon` interface +- [x] Create `JeiAddon` in `integration/jei/` with JEI recipe-display implementation +- [x] Register `JeiAddon` in `AddonManager.ADDONS` and add `showRecipesFor` dispatch method +- [x] Add hover tracking (`hoveredItem` field + per-row hit-test) to `CraftQueueOverlay` +- [x] Add `TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE` to `Constants` and `SHOW_RECIPE_MAPPING` to `ModKeyBindings` +- [x] Handle `SHOW_RECIPE_MAPPING` in `ClientEventHandler.onKeyInput` +- [x] Add "Show Recipe" translation string to all three locales in `CTLangProvider` +- [x] Run `./gradlew data` to regenerate committed lang JSON files From ca2d92b8f4c2bf308f8fe9a19784c96c53538697 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 22:19:59 -0700 Subject: [PATCH 3/6] feat: show hovered item in JEI --- .../assets/crafttracker/lang/de_de.json | 1 + .../assets/crafttracker/lang/en_gb.json | 1 + .../assets/crafttracker/lang/en_us.json | 1 + .../client/event/ClientEventHandler.java | 16 ++++++++ .../client/overlay/CraftQueueOverlay.java | 24 ++++++++++++ .../crafttracker/common/Constants.java | 1 + .../common/registry/ModKeyBindings.java | 2 + .../crafttracker/data/CTLangProvider.java | 3 ++ .../integration/RecipeViewerProvider.java | 18 +++++++++ .../integration/RecipeViewerRegistry.java | 30 +++++++++++++++ .../integration/jei/CTPlugin.java | 2 + .../jei/JeiRecipeViewerProvider.java | 38 +++++++++++++++++++ 12 files changed, 137 insertions(+) create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerProvider.java create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerRegistry.java create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiRecipeViewerProvider.java diff --git a/src/generated/resources/assets/crafttracker/lang/de_de.json b/src/generated/resources/assets/crafttracker/lang/de_de.json index 1a29745..cf573be 100644 --- a/src/generated/resources/assets/crafttracker/lang/de_de.json +++ b/src/generated/resources/assets/crafttracker/lang/de_de.json @@ -51,6 +51,7 @@ "key.crafttracker.clearShoppingList": "Einkaufsliste l\u00F6schen", "key.crafttracker.openQueueManager": "Warteschlangenmanager \u00F6ffnen", "key.crafttracker.populateShoppingList": "Einkaufsliste f\u00FCllen", + "key.crafttracker.showRecipe": "Rezept anzeigen", "key.crafttracker.toggleCraftQueue": "Herstellungswarteschlange umschalten", "key.crafttracker.toggleShoppingList": "Einkaufsliste umschalten" } \ No newline at end of file diff --git a/src/generated/resources/assets/crafttracker/lang/en_gb.json b/src/generated/resources/assets/crafttracker/lang/en_gb.json index 9f9d73d..823e571 100644 --- a/src/generated/resources/assets/crafttracker/lang/en_gb.json +++ b/src/generated/resources/assets/crafttracker/lang/en_gb.json @@ -51,6 +51,7 @@ "key.crafttracker.clearShoppingList": "Clear Shopping List", "key.crafttracker.openQueueManager": "Open Queue Manager", "key.crafttracker.populateShoppingList": "Populate Shopping List", + "key.crafttracker.showRecipe": "Show Recipe", "key.crafttracker.toggleCraftQueue": "Toggle Craft Queue", "key.crafttracker.toggleShoppingList": "Toggle Shopping List" } \ No newline at end of file diff --git a/src/generated/resources/assets/crafttracker/lang/en_us.json b/src/generated/resources/assets/crafttracker/lang/en_us.json index 9f9d73d..823e571 100644 --- a/src/generated/resources/assets/crafttracker/lang/en_us.json +++ b/src/generated/resources/assets/crafttracker/lang/en_us.json @@ -51,6 +51,7 @@ "key.crafttracker.clearShoppingList": "Clear Shopping List", "key.crafttracker.openQueueManager": "Open Queue Manager", "key.crafttracker.populateShoppingList": "Populate Shopping List", + "key.crafttracker.showRecipe": "Show Recipe", "key.crafttracker.toggleCraftQueue": "Toggle Craft Queue", "key.crafttracker.toggleShoppingList": "Toggle Shopping List" } \ No newline at end of file diff --git a/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java b/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java index eda457b..d826c2a 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java @@ -1,6 +1,7 @@ package com.sweetrpg.crafttracker.client.event; import com.sweetrpg.crafttracker.CraftTracker; +import com.sweetrpg.crafttracker.client.overlay.CraftQueueOverlay; import com.sweetrpg.crafttracker.client.screen.QueueManagementScreen; import com.sweetrpg.crafttracker.common.Constants; import com.sweetrpg.crafttracker.common.Runtime; @@ -13,6 +14,7 @@ import com.sweetrpg.crafttracker.common.util.InventoryUtil; import com.sweetrpg.crafttracker.common.util.KeyUtil; import com.sweetrpg.crafttracker.integration.HoverProviderRegistry; +import com.sweetrpg.crafttracker.integration.RecipeViewerRegistry; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; @@ -92,6 +94,11 @@ else if(KeyUtil.isKeyDown(event.getKey()) && CraftTracker.LOGGER.debug("#onKeyInput: ADD_TO_QUEUE_MAPPING"); handleAddToQueue(); + } else if (KeyUtil.isKeyDown(event.getKey()) && + ModKeyBindings.SHOW_RECIPE_MAPPING.matches(event.getKey(), event.getScanCode())) { + CraftTracker.LOGGER.debug("#onKeyInput: SHOW_RECIPE_MAPPING"); + + handleShowRecipe(); } } } @@ -201,6 +208,15 @@ private static void handleAddToQueue() { }); } + private static void handleShowRecipe() { + CraftTracker.LOGGER.debug("#handleShowRecipe"); + + var hovered = CraftQueueOverlay.hoveredItem; + if (!hovered.isEmpty()) { + RecipeViewerRegistry.showRecipesFor(hovered); + } + } + @SubscribeEvent public void onInputEvent(final MovementInputUpdateEvent event) { CraftTracker.LOGGER.trace("#onInputEvent: {}", event); diff --git a/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java b/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java index 599cfe7..fa3f970 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java @@ -37,10 +37,21 @@ public class CraftQueueOverlay { static int LINE_HEIGHT = 16; static int TEXT_HEIGHT = 12; static int MAX_STRING_LENGTH = 40; + static int ITEM_ICON_SIZE = 16; + + /** + * The item currently under the mouse in the overlay; {@link ItemStack#EMPTY} when none. + */ + public static ItemStack hoveredItem = ItemStack.EMPTY; public static final IIngameOverlay CRAFT_QUEUE = (gui, poseStack, partialTicks, width, height) -> { CraftTracker.LOGGER.trace("CRAFT_QUEUE"); + hoveredItem = ItemStack.EMPTY; + double guiScale = Minecraft.getInstance().getWindow().getGuiScale(); + int mouseX = (int) (Minecraft.getInstance().mouseHandler.xpos() / guiScale); + int mouseY = (int) (Minecraft.getInstance().mouseHandler.ypos() / guiScale); + Minecraft mc = Minecraft.getInstance(); CraftingQueueManager mgr = CraftingQueueManager.INSTANCE; List products = mgr.getEndProducts().stream().sorted((i1, i2) -> { @@ -141,6 +152,10 @@ public class CraftQueueOverlay { GuiComponent.drawString(poseStack, gui.getFont(), text, x + ITEM_NAME_X_OFFSET, yPos + 4, TEXT_COLOR); } + if (isMouseOver(mouseX, mouseY, x + SECTION_X_OFFSET, yPos)) { + hoveredItem = stack; + } + yPos += LINE_HEIGHT + 2; CraftTracker.LOGGER.trace("yPos (product item {}): {}", i, yPos); } @@ -201,6 +216,10 @@ public class CraftQueueOverlay { GuiComponent.drawString(poseStack, gui.getFont(), text, x + ITEM_NAME_X_OFFSET, yPos + 4, TEXT_COLOR); } + if (isMouseOver(mouseX, mouseY, x + SECTION_X_OFFSET, yPos)) { + hoveredItem = stack; + } + yPos += LINE_HEIGHT + 2; CraftTracker.LOGGER.trace("yPos (intermediates item {}): {}", i, yPos); } @@ -321,6 +340,11 @@ public class CraftQueueOverlay { } }; + private static boolean isMouseOver(int mouseX, int mouseY, int itemX, int itemY) { + return mouseX >= itemX && mouseX < itemX + ITEM_ICON_SIZE + && mouseY >= itemY && mouseY < itemY + ITEM_ICON_SIZE; + } + private static Recipe getRecipeFor(CraftingQueueProduct product) { try { return product.getRecipes().get(product.getIndex()); diff --git a/src/main/java/com/sweetrpg/crafttracker/common/Constants.java b/src/main/java/com/sweetrpg/crafttracker/common/Constants.java index 397ad8e..aa59c21 100644 --- a/src/main/java/com/sweetrpg/crafttracker/common/Constants.java +++ b/src/main/java/com/sweetrpg/crafttracker/common/Constants.java @@ -62,6 +62,7 @@ public class Constants { public static final String TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE = "key.crafttracker.openQueueManager"; public static final String TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE = "key.crafttracker.populateShoppingList"; public static final String TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE = "key.crafttracker.clearShoppingList"; + public static final String TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE = "key.crafttracker.showRecipe"; // Config public static final String TRANSLATION_KEY_CONFIG_CLIENT_CALC_DEPTH = "crafttracker.config.client.calculation_depth"; diff --git a/src/main/java/com/sweetrpg/crafttracker/common/registry/ModKeyBindings.java b/src/main/java/com/sweetrpg/crafttracker/common/registry/ModKeyBindings.java index e9bf463..21f294c 100644 --- a/src/main/java/com/sweetrpg/crafttracker/common/registry/ModKeyBindings.java +++ b/src/main/java/com/sweetrpg/crafttracker/common/registry/ModKeyBindings.java @@ -14,6 +14,7 @@ public class ModKeyBindings { public static final KeyMapping OPEN_QUEUE_MANAGER_MAPPING = new KeyMapping(Constants.TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE, KeyConflictContext.GUI, InputConstants.Type.KEYSYM, InputConstants.KEY_M, Constants.KEY_BINDINGS_CATEGORY_TITLE); public static final KeyMapping POPULATE_SHOPPING_LIST_MAPPING = new KeyMapping(Constants.TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE, KeyConflictContext.GUI, InputConstants.Type.KEYSYM, InputConstants.KEY_P, Constants.KEY_BINDINGS_CATEGORY_TITLE); public static final KeyMapping CLEAR_SHOPPING_LIST_MAPPING = new KeyMapping(Constants.TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE, KeyConflictContext.GUI, InputConstants.Type.KEYSYM, InputConstants.KEY_K, Constants.KEY_BINDINGS_CATEGORY_TITLE); + public static final KeyMapping SHOW_RECIPE_MAPPING = new KeyMapping(Constants.TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE, KeyConflictContext.GUI, InputConstants.Type.KEYSYM, InputConstants.KEY_R, Constants.KEY_BINDINGS_CATEGORY_TITLE); public static void init() { ClientRegistry.registerKeyBinding(ADD_TO_QUEUE_MAPPING); @@ -22,6 +23,7 @@ public static void init() { ClientRegistry.registerKeyBinding(OPEN_QUEUE_MANAGER_MAPPING); ClientRegistry.registerKeyBinding(POPULATE_SHOPPING_LIST_MAPPING); ClientRegistry.registerKeyBinding(CLEAR_SHOPPING_LIST_MAPPING); + ClientRegistry.registerKeyBinding(SHOW_RECIPE_MAPPING); } } diff --git a/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java b/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java index da1dbf7..202818e 100644 --- a/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java +++ b/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java @@ -56,6 +56,7 @@ private void processENUS() { add(Constants.TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE, "Open Queue Manager"); add(Constants.TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE, "Populate Shopping List"); add(Constants.TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE, "Clear Shopping List"); + add(Constants.TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE, "Show Recipe"); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "The craft queue overlay will now be hidden."); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "The craft queue overlay will now be shown."); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "The craft queue overlay mode is dynamic."); @@ -117,6 +118,7 @@ private void processENGB() { add(Constants.TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE, "Open Queue Manager"); add(Constants.TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE, "Populate Shopping List"); add(Constants.TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE, "Clear Shopping List"); + add(Constants.TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE, "Show Recipe"); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "The craft queue overlay will now be hidden."); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "The craft queue overlay will now be shown."); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "The craft queue overlay mode is dynamic."); @@ -178,6 +180,7 @@ private void processDEDE() { add(Constants.TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE, "Warteschlangenmanager öffnen"); add(Constants.TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE, "Einkaufsliste füllen"); add(Constants.TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE, "Einkaufsliste löschen"); + add(Constants.TRANSLATION_KEY_BINDINGS_SHOW_RECIPE_TITLE, "Rezept anzeigen"); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "Die Überlagerung der Handwerksliste wird nun ausgeblendet."); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "Jetzt wird die Überlagerung mit der Handwerksliste angezeigt."); add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "Der Überlagerungsmodus der Handwerksliste ist dynamisch."); diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerProvider.java b/src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerProvider.java new file mode 100644 index 0000000..f82752d --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerProvider.java @@ -0,0 +1,18 @@ +package com.sweetrpg.crafttracker.integration; + +import net.minecraft.world.item.ItemStack; + +/** + * Opens recipes for a given item in an installed item viewer. + * Implementations are registered in {@link RecipeViewerRegistry}. + * Only JEI-specific code may import JEI API types; all other code uses this interface. + */ +public interface RecipeViewerProvider { + + String getName(); + + /** + * Shows recipes for {@code stack} in the item viewer. No-op if the viewer is unavailable. + */ + void showRecipesFor(ItemStack stack); +} diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerRegistry.java b/src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerRegistry.java new file mode 100644 index 0000000..8987372 --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/RecipeViewerRegistry.java @@ -0,0 +1,30 @@ +package com.sweetrpg.crafttracker.integration; + +import com.sweetrpg.crafttracker.CraftTracker; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds registered {@link RecipeViewerProvider}s. + * Call {@link #showRecipesFor(ItemStack)} to dispatch to all registered providers. + */ +public class RecipeViewerRegistry { + + private static final List PROVIDERS = new ArrayList<>(); + + public static void register(RecipeViewerProvider provider) { + PROVIDERS.add(provider); + } + + public static void showRecipesFor(ItemStack stack) { + for (RecipeViewerProvider provider : PROVIDERS) { + try { + provider.showRecipesFor(stack); + } catch (RuntimeException e) { + CraftTracker.LOGGER.warn("RecipeViewerRegistry: {} failed to show recipes", provider.getName(), e); + } + } + } +} diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/jei/CTPlugin.java b/src/main/java/com/sweetrpg/crafttracker/integration/jei/CTPlugin.java index 106a9d2..58648ca 100644 --- a/src/main/java/com/sweetrpg/crafttracker/integration/jei/CTPlugin.java +++ b/src/main/java/com/sweetrpg/crafttracker/integration/jei/CTPlugin.java @@ -3,6 +3,7 @@ import com.sweetrpg.crafttracker.CraftTracker; import com.sweetrpg.crafttracker.common.Constants; import com.sweetrpg.crafttracker.integration.HoverProviderRegistry; +import com.sweetrpg.crafttracker.integration.RecipeViewerRegistry; import mezz.jei.api.IModPlugin; import mezz.jei.api.JeiPlugin; import mezz.jei.api.registration.*; @@ -75,5 +76,6 @@ public void registerAdvanced(IAdvancedRegistration registration) { public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { CraftTracker.LOGGER.debug("CTPlugin#onRuntimeAvailable: {}", jeiRuntime); HoverProviderRegistry.register(new JeiHoverProvider(jeiRuntime)); + RecipeViewerRegistry.register(new JeiRecipeViewerProvider(jeiRuntime)); } } diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiRecipeViewerProvider.java b/src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiRecipeViewerProvider.java new file mode 100644 index 0000000..b451e60 --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiRecipeViewerProvider.java @@ -0,0 +1,38 @@ +package com.sweetrpg.crafttracker.integration.jei; + +import com.sweetrpg.crafttracker.CraftTracker; +import com.sweetrpg.crafttracker.integration.RecipeViewerProvider; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.recipe.IFocus; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.runtime.IJeiRuntime; +import net.minecraft.world.item.ItemStack; + +/** + * Shows recipes in JEI. Only this class and {@link CTPlugin} may import JEI API types. + */ +public class JeiRecipeViewerProvider implements RecipeViewerProvider { + + private final IJeiRuntime jeiRuntime; + + public JeiRecipeViewerProvider(IJeiRuntime jeiRuntime) { + this.jeiRuntime = jeiRuntime; + } + + @Override + public String getName() { + return "jei"; + } + + @Override + public void showRecipesFor(ItemStack stack) { + if (stack.isEmpty()) { + CraftTracker.LOGGER.debug("JeiRecipeViewerProvider#showRecipesFor: empty stack, skipping"); + return; + } + IFocus focus = jeiRuntime.getJeiHelpers() + .getFocusFactory() + .createFocus(RecipeIngredientRole.OUTPUT, VanillaTypes.ITEM_STACK, stack); + jeiRuntime.getRecipesGui().show(focus); + } +} From 2d44973d6201944fa8b3b1889147b1370b5d0268 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 22:20:14 -0700 Subject: [PATCH 4/6] chore: update release info and changelog --- .release-info/1.18/BUMP_LEVEL | 2 +- CHANGELOG/1.18/current.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.release-info/1.18/BUMP_LEVEL b/.release-info/1.18/BUMP_LEVEL index 9eb7b90..acb503f 100644 --- a/.release-info/1.18/BUMP_LEVEL +++ b/.release-info/1.18/BUMP_LEVEL @@ -1 +1 @@ -patch +minor diff --git a/CHANGELOG/1.18/current.md b/CHANGELOG/1.18/current.md index 77dcc74..751fb64 100644 --- a/CHANGELOG/1.18/current.md +++ b/CHANGELOG/1.18/current.md @@ -1 +1,2 @@ - [NEW] JEI is now an optional dependency +- [NEW] Open the hovered item in JEI \ No newline at end of file From 8b33c51283ace714dfd1df913b7bb0d4ba705d22 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Mon, 22 Jun 2026 06:33:06 -0700 Subject: [PATCH 5/6] chore: only show (x#) for iterations > 1 --- .../crafttracker/client/overlay/CraftQueueOverlay.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java b/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java index fa3f970..9545ce0 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java @@ -144,7 +144,11 @@ public class CraftQueueOverlay { itemRenderer.renderAndDecorateFakeItem(stack, x + SECTION_X_OFFSET, yPos); itemRenderer.renderGuiItemDecorations(mc.font, stack, x + SECTION_X_OFFSET, yPos); - String text = String.format("%s (x%d)", item.getDescription().getString(MAX_STRING_LENGTH), p.getIterations()); + String iterText = ""; + if (p.getIterations() > 1) { + iterText = String.format(" (x%d)", p.getIterations()); + } + String text = String.format("%s%s", item.getDescription().getString(MAX_STRING_LENGTH), iterText); GuiComponent.drawString(poseStack, gui.getFont(), text, x + ITEM_NAME_X_OFFSET, yPos + 4, TEXT_COLOR); } catch (RuntimeException e) { From 66c74a89ccd38e05c9a6821b6bbff25995f40b54 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Tue, 23 Jun 2026 06:43:20 -0700 Subject: [PATCH 6/6] chore: initialize queue manager on client login --- .../client/event/ClientEventHandler.java | 3 +++ .../client/overlay/CraftQueueOverlay.java | 7 +++++++ .../common/manager/CraftingQueueManager.java | 17 +++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java b/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java index d826c2a..117c5f0 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java @@ -31,6 +31,7 @@ public class ClientEventHandler { private static boolean managersLoaded = false; + @SubscribeEvent public static void onClientLogin(final ClientPlayerNetworkEvent.LoggedInEvent event) { CraftTracker.LOGGER.debug("#onClientLogin"); if (!managersLoaded) { @@ -43,9 +44,11 @@ public static void onClientLogin(final ClientPlayerNetworkEvent.LoggedInEvent ev } } + @SubscribeEvent public static void onClientLogout(final ClientPlayerNetworkEvent.LoggedOutEvent event) { CraftTracker.LOGGER.debug("#onClientLogout"); managersLoaded = false; + CraftingQueueManager.INSTANCE.unload(); } public static void onKeyInput(final InputEvent.KeyInputEvent event) { diff --git a/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java b/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java index 9545ce0..df94f7f 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/overlay/CraftQueueOverlay.java @@ -54,6 +54,8 @@ public class CraftQueueOverlay { Minecraft mc = Minecraft.getInstance(); CraftingQueueManager mgr = CraftingQueueManager.INSTANCE; + if (!mgr.isReady()) return; + List products = mgr.getEndProducts().stream().sorted((i1, i2) -> { Item item1 = ForgeRegistries.ITEMS.getValue(i1.getProductId()); if(item1 == null) return 0; @@ -152,6 +154,7 @@ public class CraftQueueOverlay { GuiComponent.drawString(poseStack, gui.getFont(), text, x + ITEM_NAME_X_OFFSET, yPos + 4, TEXT_COLOR); } catch (RuntimeException e) { + CraftTracker.LOGGER.error("Error rendering recipe item: {}", p.getProductId(), e); String text = I18n.get(Constants.TRANSLATION_KEY_GUI_NO_RECIPES, p.getProductId().toString(), p.getIndex()); GuiComponent.drawString(poseStack, gui.getFont(), text, x + ITEM_NAME_X_OFFSET, yPos + 4, TEXT_COLOR); } @@ -351,6 +354,10 @@ private static boolean isMouseOver(int mouseX, int mouseY, int itemX, int itemY) private static Recipe getRecipeFor(CraftingQueueProduct product) { try { +// if(product.getRecipes().isEmpty()) { +// CraftTracker.LOGGER.debug("No recipes found for product: {}; attempting to acquire again", product.getProductId()); +// product.setRecipes(RecipeUtil.getRecipesFor(product.getProductId())); +// } return product.getRecipes().get(product.getIndex()); } catch (RuntimeException e) { diff --git a/src/main/java/com/sweetrpg/crafttracker/common/manager/CraftingQueueManager.java b/src/main/java/com/sweetrpg/crafttracker/common/manager/CraftingQueueManager.java index 4ea92ec..c860f1d 100644 --- a/src/main/java/com/sweetrpg/crafttracker/common/manager/CraftingQueueManager.java +++ b/src/main/java/com/sweetrpg/crafttracker/common/manager/CraftingQueueManager.java @@ -45,6 +45,7 @@ public class CraftingQueueManager { private Map intermediateProducts = new HashMap<>(); private Map rawMaterials = new HashMap<>(); private Map fuel = new HashMap<>(); + private boolean ready = false; /** * Default constructor. @@ -52,6 +53,20 @@ public class CraftingQueueManager { public CraftingQueueManager() { } + /** + * Returns true after {@link #load} has completed for the current session. + */ + public boolean isReady() { + return ready; + } + + /** + * Marks the manager as uninitialized; call on player logout. + */ + public void unload() { + this.ready = false; + } + /** * Load the last persisted crafting queue from storage. * @@ -77,6 +92,8 @@ public void load(Player player) { catch (IOException e) { CraftTracker.LOGGER.error("An error occurred while loading crafting queue [" + file + "]", e); } + + this.ready = true; } /**