Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .release-info/1.18/BUMP_LEVEL
Original file line number Diff line number Diff line change
@@ -1 +1 @@
patch
minor
1 change: 1 addition & 0 deletions CHANGELOG/1.18/current.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- [NEW] JEI is now an optional dependency
- [NEW] Open the hovered item in JEI
2 changes: 1 addition & 1 deletion LICENSE.txt → LICENSE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
135 changes: 135 additions & 0 deletions openspec/changes/open-recipe-in-item-viewer/design.md
Original file line number Diff line number Diff line change
@@ -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<ItemStack> 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.
10 changes: 10 additions & 0 deletions openspec/changes/open-recipe-in-item-viewer/tasks.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -29,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) {
Expand All @@ -41,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) {
Expand Down Expand Up @@ -92,6 +97,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();
}
}
}
Expand Down Expand Up @@ -201,6 +211,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,25 @@ 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;
if (!mgr.isReady()) return;

List<CraftingQueueProduct> products = mgr.getEndProducts().stream().sorted((i1, i2) -> {
Item item1 = ForgeRegistries.ITEMS.getValue(i1.getProductId());
if(item1 == null) return 0;
Expand Down Expand Up @@ -133,14 +146,23 @@ 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) {
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);
}

if (isMouseOver(mouseX, mouseY, x + SECTION_X_OFFSET, yPos)) {
hoveredItem = stack;
}

yPos += LINE_HEIGHT + 2;
CraftTracker.LOGGER.trace("yPos (product item {}): {}", i, yPos);
}
Expand Down Expand Up @@ -201,6 +223,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);
}
Expand Down Expand Up @@ -321,8 +347,17 @@ 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 {
// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,28 @@ public class CraftingQueueManager {
private Map<ResourceLocation, CraftingQueueItem> intermediateProducts = new HashMap<>();
private Map<ResourceLocation, CraftingQueueItem> rawMaterials = new HashMap<>();
private Map<ResourceLocation, CraftingQueueItem> fuel = new HashMap<>();
private boolean ready = false;

/**
* Default constructor.
*/
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.
*
Expand All @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

}
Loading
Loading