From cb14dcb2655e014676f062f6a24cf7aea71c0127 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 07:00:43 -0700 Subject: [PATCH 01/18] feat: remove JEI hard dependency Add hover providers for vanilla and JEI to get item information Add item picker text box to queue manager Refs: https://github.com/sweetrpg/CraftTracker/issues/97 --- .../remove-jei-hard-dependency/tasks.md | 48 ++++---- .../crafttracker/client/ClientSetup.java | 6 + .../client/event/ClientEventHandler.java | 52 +++++---- .../client/screen/QueueManagementScreen.java | 107 ++++++++++++++++++ .../crafttracker/common/Constants.java | 2 + .../crafttracker/data/CTLangProvider.java | 6 + .../integration/HoverItemProvider.java | 22 ++++ .../integration/HoverProviderRegistry.java | 33 ++++++ .../integration/VanillaSlotHoverProvider.java | 36 ++++++ .../integration/jei/CTPlugin.java | 19 +--- .../integration/jei/JeiHoverProvider.java | 40 +++++++ .../resources/META-INF/accesstransformer.cfg | 3 + src/main/resources/META-INF/mods.toml | 2 +- 13 files changed, 318 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/HoverItemProvider.java create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/HoverProviderRegistry.java create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/VanillaSlotHoverProvider.java create mode 100644 src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiHoverProvider.java diff --git a/openspec/changes/remove-jei-hard-dependency/tasks.md b/openspec/changes/remove-jei-hard-dependency/tasks.md index 08ee42c..41a0065 100644 --- a/openspec/changes/remove-jei-hard-dependency/tasks.md +++ b/openspec/changes/remove-jei-hard-dependency/tasks.md @@ -1,41 +1,51 @@ ## 1. Hover provider abstraction -- [ ] 1.1 Add a `HoverItemProvider` interface under `integration/` returning the item under the mouse for a given screen + mouse position (priority + `getName()`) -- [ ] 1.2 Implement `VanillaSlotHoverProvider` using `AbstractContainerScreen.getSlotUnderMouse()` → `slot.getItem()`; register it unconditionally -- [ ] 1.3 Implement `JeiHoverProvider` wrapping `getIngredientListOverlay().getIngredientUnderMouse()`; this is the only client-path class that imports JEI API types -- [ ] 1.4 Add a registry/lookup that returns registered providers in priority order, with the JEI provider ahead of the native provider +- [x] 1.1 Add a `HoverItemProvider` interface under `integration/` returning the item under the mouse for a given + screen + mouse position (priority + `getName()`) +- [x] 1.2 Implement `VanillaSlotHoverProvider` using `AbstractContainerScreen.getSlotUnderMouse()` → `slot.getItem()`; + register it unconditionally +- [x] 1.3 Implement `JeiHoverProvider` wrapping `getIngredientListOverlay().getIngredientUnderMouse()`; this is the only + client-path class that imports JEI API types +- [x] 1.4 Add a registry/lookup that returns registered providers in priority order, with the JEI provider ahead of the + native provider ## 2. JEI classloading isolation & registration -- [ ] 2.1 Hand the `IJeiRuntime` from `CTPlugin.onRuntimeAvailable` to `JeiHoverProvider` instead of exposing `CTPlugin.jeiRuntime` to `ClientEventHandler` -- [ ] 2.2 Register `JeiHoverProvider` only behind `ModList.get().isLoaded("jei")`, wired through the existing `Addon`/`AddonManager` integration layer -- [ ] 2.3 Confirm no class on a no-JEI code path statically references `CTPlugin` or any JEI API type +- [x] 2.1 Hand the `IJeiRuntime` from `CTPlugin.onRuntimeAvailable` to `JeiHoverProvider` instead of exposing + `CTPlugin.jeiRuntime` to `ClientEventHandler` +- [x] 2.2 Register `JeiHoverProvider` only behind `ModList.get().isLoaded("jei")`, wired through the existing `Addon`/ + `AddonManager` integration layer +- [x] 2.3 Confirm no class on a no-JEI code path statically references `CTPlugin` or any JEI API type ## 3. Repoint add-to-queue -- [ ] 3.1 Rewrite `ClientEventHandler#handleAddToQueue` to query the provider lookup, take the first resolved item, and call `CraftingQueueManager.INSTANCE.addProduct(player, id, 1)` + send the `QUEUE_ITEM` advancement -- [ ] 3.2 Remove the JEI import and `CTPlugin.jeiRuntime` reference from `ClientEventHandler` -- [ ] 3.3 Confirm `onKeyInput` still gates the action to container screens (extend the allowed screen list if needed) +- [x] 3.1 Rewrite `ClientEventHandler#handleAddToQueue` to query the provider lookup, take the first resolved item, and + call `CraftingQueueManager.INSTANCE.addProduct(player, id, 1)` + send the `QUEUE_ITEM` advancement +- [x] 3.2 Remove the JEI import and `CTPlugin.jeiRuntime` reference from `ClientEventHandler` +- [x] 3.3 Confirm `onKeyInput` still gates the action to container screens (extend the allowed screen list if needed) ## 4. JEI-independent data lifecycle -- [ ] 4.1 Add a client world-join / login listener that loads `CraftingQueueManager` and `ShoppingListManager` for the client player, registered in the existing client event wiring -- [ ] 4.2 Remove the `load(player)` calls from `CTPlugin.onRuntimeAvailable` -- [ ] 4.3 Ensure loading is idempotent and fires once per world entry +- [x] 4.1 Add a client world-join / login listener that loads `CraftingQueueManager` and `ShoppingListManager` for the + client player, registered in the existing client event wiring +- [x] 4.2 Remove the `load(player)` calls from `CTPlugin.onRuntimeAvailable` +- [x] 4.3 Ensure loading is idempotent and fires once per world entry ## 5. Native item picker in Queue Management screen -- [ ] 5.1 Add a search box + scrollable item list to `QueueManagementScreen` backed by `ForgeRegistries.ITEMS`, filtered by the search term -- [ ] 5.2 On selection, call `addProduct(player, itemId, 1)` and refresh the screen/queue -- [ ] 5.3 Add any new translation keys to `common/Constants` and `CTLangProvider`, then regenerate with `./gradlew data` +- [x] 5.1 Add a search box + scrollable item list to `QueueManagementScreen` backed by `ForgeRegistries.ITEMS`, filtered + by the search term +- [x] 5.2 On selection, call `addProduct(player, itemId, 1)` and refresh the screen/queue +- [x] 5.3 Add any new translation keys to `common/Constants` and `CTLangProvider`, then regenerate with `./gradlew data` ## 6. Dependency metadata -- [ ] 6.1 Set `mandatory=false` for the `jei` dependency block in `META-INF/mods.toml` -- [ ] 6.2 Change CurseForge `requiredDependency 'jei'` and Modrinth `required.project "jei"` to their optional equivalents in `build.gradle` (keep `runtimeOnly` JEI for dev) +- [x] 6.1 Set `mandatory=false` for the `jei` dependency block in `META-INF/mods.toml` +- [x] 6.2 Change CurseForge `requiredDependency 'jei'` and Modrinth `required.project "jei"` to their optional + equivalents in `build.gradle` (keep `runtimeOnly` JEI for dev) ## 7. Verification -- [ ] 7.1 `./gradlew build` and `./gradlew test` pass +- [x] 7.1 `./gradlew build` and `./gradlew test` pass - [ ] 7.2 Launch `./gradlew client` with JEI present: verify hover-add from container slots, hover-add from the JEI panel, the picker, and that queue/list load on join - [ ] 7.3 Launch the client with JEI removed from `runtimeOnly`: verify the mod loads (no `NoClassDefFoundError`), hover-add from slots works, the picker works, and queue/list load on join diff --git a/src/main/java/com/sweetrpg/crafttracker/client/ClientSetup.java b/src/main/java/com/sweetrpg/crafttracker/client/ClientSetup.java index 94ce083..22737f4 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/ClientSetup.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/ClientSetup.java @@ -4,6 +4,8 @@ import com.sweetrpg.crafttracker.client.overlay.CraftQueueOverlay; import com.sweetrpg.crafttracker.client.overlay.ShoppingListOverlay; import com.sweetrpg.crafttracker.common.registry.ModKeyBindings; +import com.sweetrpg.crafttracker.integration.HoverProviderRegistry; +import com.sweetrpg.crafttracker.integration.VanillaSlotHoverProvider; import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; import net.minecraftforge.client.gui.OverlayRegistry; import net.minecraftforge.common.MinecraftForge; @@ -23,10 +25,14 @@ public static void addKeyBindings(final FMLClientSetupEvent event) { MinecraftForge.EVENT_BUS.addListener(ClientEventHandler::onScreenInit); MinecraftForge.EVENT_BUS.addListener(ClientEventHandler::onKeyInput); + MinecraftForge.EVENT_BUS.addListener(ClientEventHandler::onClientLogin); + MinecraftForge.EVENT_BUS.addListener(ClientEventHandler::onClientLogout); OverlayRegistry.registerOverlayAbove(HOTBAR_ELEMENT, "craft_queue", CraftQueueOverlay.CRAFT_QUEUE); OverlayRegistry.registerOverlayAbove(HOTBAR_ELEMENT, "shopping_list", ShoppingListOverlay.SHOPPING_LIST); ModKeyBindings.init(); + + HoverProviderRegistry.register(new VanillaSlotHoverProvider()); } } 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 9af7918..eda457b 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/event/ClientEventHandler.java @@ -12,15 +12,14 @@ import com.sweetrpg.crafttracker.common.registry.ModKeyBindings; import com.sweetrpg.crafttracker.common.util.InventoryUtil; import com.sweetrpg.crafttracker.common.util.KeyUtil; -import com.sweetrpg.crafttracker.integration.jei.CTPlugin; +import com.sweetrpg.crafttracker.integration.HoverProviderRegistry; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; import net.minecraft.client.gui.screens.inventory.InventoryScreen; import net.minecraft.network.chat.TranslatableComponent; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.ItemStack; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; import net.minecraftforge.client.event.InputEvent; import net.minecraftforge.client.event.MovementInputUpdateEvent; import net.minecraftforge.client.event.ScreenEvent; @@ -28,6 +27,25 @@ public class ClientEventHandler { + private static boolean managersLoaded = false; + + public static void onClientLogin(final ClientPlayerNetworkEvent.LoggedInEvent event) { + CraftTracker.LOGGER.debug("#onClientLogin"); + if (!managersLoaded) { + managersLoaded = true; + var player = event.getPlayer(); + if (player != null) { + CraftingQueueManager.INSTANCE.load(player); + ShoppingListManager.INSTANCE.load(player); + } + } + } + + public static void onClientLogout(final ClientPlayerNetworkEvent.LoggedOutEvent event) { + CraftTracker.LOGGER.debug("#onClientLogout"); + managersLoaded = false; + } + public static void onKeyInput(final InputEvent.KeyInputEvent event) { CraftTracker.LOGGER.trace("#onKeyInput: {}", event); @@ -69,8 +87,7 @@ else if(KeyUtil.isKeyDown(event.getKey()) && return; } - if(screen instanceof CraftingScreen || - screen instanceof InventoryScreen) { // TODO: others? + if (screen instanceof AbstractContainerScreen) { if(ModKeyBindings.ADD_TO_QUEUE_MAPPING.matches(event.getKey(), event.getScanCode())) { CraftTracker.LOGGER.debug("#onKeyInput: ADD_TO_QUEUE_MAPPING"); @@ -173,22 +190,15 @@ private static void handleToggleShoppingList() { private static void handleAddToQueue() { CraftTracker.LOGGER.debug("#handleAddToQueue"); - CTPlugin.jeiRuntime.getIngredientListOverlay().getIngredientUnderMouse() - .ifPresent(ingredient -> { - CraftTracker.LOGGER.debug("#handleAddToQueue: type {}", ingredient.getType()); - CraftTracker.LOGGER.debug("#handleAddToQueue: ingredient {}", ingredient.getIngredient()); - - if(ingredient.getIngredient() instanceof ItemStack itemStack) { - ResourceLocation res = itemStack.getItem().getRegistryName(); - CraftTracker.LOGGER.debug("#handleAddToQueue: res {}", res); - - var player = Minecraft.getInstance().player; - CraftingQueueManager.INSTANCE.addProduct(player, res, 1); + var screen = Minecraft.getInstance().screen; + if (screen == null) return; - // send advancement packet - PacketHandler.sendToServer(new AdvancementData(ModAdvancements.Key.QUEUE_ITEM)); - } - }); + HoverProviderRegistry.resolve(screen).ifPresent(res -> { + CraftTracker.LOGGER.debug("#handleAddToQueue: res {}", res); + var player = Minecraft.getInstance().player; + CraftingQueueManager.INSTANCE.addProduct(player, res, 1); + PacketHandler.sendToServer(new AdvancementData(ModAdvancements.Key.QUEUE_ITEM)); + }); } @SubscribeEvent diff --git a/src/main/java/com/sweetrpg/crafttracker/client/screen/QueueManagementScreen.java b/src/main/java/com/sweetrpg/crafttracker/client/screen/QueueManagementScreen.java index d1a28e0..6e10d8e 100644 --- a/src/main/java/com/sweetrpg/crafttracker/client/screen/QueueManagementScreen.java +++ b/src/main/java/com/sweetrpg/crafttracker/client/screen/QueueManagementScreen.java @@ -12,15 +12,19 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiComponent; import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.renderer.entity.ItemRenderer; import net.minecraft.client.resources.language.I18n; import net.minecraft.network.chat.TextComponent; import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; import net.minecraftforge.registries.ForgeRegistries; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import static com.sweetrpg.crafttracker.common.Runtime.OverlayState.SUPPRESS; @@ -42,9 +46,17 @@ public class QueueManagementScreen extends Screen { public static final int ITEM_X_QTY_OFFSET = ITEM_X_UP_BUTTON_OFFSET - (ITEM_X_QTY_WIDTH / 2); public static final int ITEM_X_DOWN_BUTTON_OFFSET = ITEM_X_QTY_OFFSET - (int) (BUTTON_SIZE * 2) - 2; + private static final int PICKER_GAP = 10; + private static final int PICKER_ITEM_HEIGHT = 20; + private static final int PICKER_MAX_VISIBLE = 8; + private static final int PICKER_MIN_WIDTH = 80; + private List productItems; private Runtime.OverlayState queueState; private Runtime.OverlayState shoppingState; + private EditBox searchBox; + private List filteredItems; + private int itemScrollOffset; public QueueManagementScreen(Player player) { super(new TranslatableComponent(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_TITLE)); @@ -69,6 +81,32 @@ public void init() { Runtime.INSTANCE.queueOverlayRequestedState = SUPPRESS; this.shoppingState = Runtime.INSTANCE.shoppingOverlayRequestedState; Runtime.INSTANCE.shoppingOverlayRequestedState = SUPPRESS; + + // item picker setup + int queueWidth = Math.max(200, this.width / 3); + int pickerX = (this.width / 2) + (queueWidth / 2) + PICKER_GAP; + int pickerWidth = this.width - pickerX - PICKER_GAP; + if (pickerWidth > PICKER_MIN_WIDTH) { + this.searchBox = new EditBox(this.font, pickerX, 20 + TITLE_HEIGHT + 4, pickerWidth, 16, new TextComponent("")); + this.searchBox.setMaxLength(50); + this.searchBox.setResponder(text -> { + this.itemScrollOffset = 0; + this.updateFilteredItems(text); + }); + addRenderableWidget(this.searchBox); + } + this.itemScrollOffset = 0; + this.updateFilteredItems(""); + } + + private void updateFilteredItems(String search) { + String lower = search.toLowerCase(); + this.filteredItems = ForgeRegistries.ITEMS.getKeys().stream() + .filter(id -> lower.isEmpty() + || id.toString().contains(lower) + || ForgeRegistries.ITEMS.getValue(id).getDescription().getString().toLowerCase().contains(lower)) + .sorted(Comparator.comparing(ResourceLocation::toString)) + .collect(Collectors.toList()); } @Override @@ -79,6 +117,11 @@ public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTic int topX = (this.width / 2) - (width / 2); int topY = 20; // (this.height / 2) - (height / 2); + // Re-anchor searchBox after renderables.clear() from button handlers + if (this.searchBox != null && !this.renderables.contains(this.searchBox)) { + addRenderableWidget(this.searchBox); + } + this.renderBackground(poseStack); // title @@ -182,9 +225,73 @@ public void renderToolTip(PoseStack pPoseStack, int pMouseX, int pMouseY) { this.addRenderableWidget(button); } + // item picker panel (right of queue) + int pickerX = topX + width + PICKER_GAP; + int pickerWidth = this.width - pickerX - PICKER_GAP; + if (pickerWidth > PICKER_MIN_WIDTH && this.filteredItems != null) { + GuiComponent.drawCenteredString(poseStack, this.font, + I18n.get(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_TITLE), + pickerX + pickerWidth / 2, topY + 2, TITLE_COLOR); + this.font.draw(poseStack, + I18n.get(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_SEARCH), + pickerX, topY + TITLE_HEIGHT + 2, TITLE_COLOR); + + int listY = topY + TITLE_HEIGHT + 22; // below label + search box + int visible = Math.min(PICKER_MAX_VISIBLE, this.filteredItems.size() - this.itemScrollOffset); + for (int i = 0; i < visible; i++) { + var itemId = this.filteredItems.get(i + this.itemScrollOffset); + var item = ForgeRegistries.ITEMS.getValue(itemId); + int rowY = listY + i * PICKER_ITEM_HEIGHT; + + GuiComponent.fill(poseStack, pickerX, rowY, pickerX + pickerWidth, rowY + PICKER_ITEM_HEIGHT, BACKGROUND_COLOR); + + ItemRenderer itemRenderer = this.minecraft.getItemRenderer(); + itemRenderer.renderAndDecorateFakeItem(item.getDefaultInstance(), pickerX + 2, rowY + 2); + + this.font.draw(poseStack, item.getDescription(), pickerX + 20, rowY + 6, ITEM_COLOR); + } + } + super.render(poseStack, mouseX, mouseY, partialTicks); } + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && this.filteredItems != null) { + int queueWidth = Math.max(200, this.width / 3); + int pickerX = (this.width / 2) + (queueWidth / 2) + PICKER_GAP; + int pickerWidth = this.width - pickerX - PICKER_GAP; + int listY = 20 + TITLE_HEIGHT + 22; + + if (pickerWidth > PICKER_MIN_WIDTH && mouseX >= pickerX && mouseX < pickerX + pickerWidth + && mouseY >= listY) { + int idx = (int) (mouseY - listY) / PICKER_ITEM_HEIGHT + this.itemScrollOffset; + if (idx >= 0 && idx < this.filteredItems.size()) { + var itemId = this.filteredItems.get(idx); + CraftingQueueManager.INSTANCE.addProduct(this.player, itemId, 1); + this.productItems = CraftingQueueManager.INSTANCE.getEndProducts(); + return true; + } + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (this.filteredItems != null) { + int queueWidth = Math.max(200, this.width / 3); + int pickerX = (this.width / 2) + (queueWidth / 2) + PICKER_GAP; + int pickerWidth = this.width - pickerX - PICKER_GAP; + if (pickerWidth > PICKER_MIN_WIDTH && mouseX >= pickerX) { + int maxOffset = Math.max(0, this.filteredItems.size() - PICKER_MAX_VISIBLE); + this.itemScrollOffset = (int) Math.max(0, Math.min(maxOffset, this.itemScrollOffset - delta)); + return true; + } + } + return super.mouseScrolled(mouseX, mouseY, delta); + } + @Override public void removed() { super.removed(); diff --git a/src/main/java/com/sweetrpg/crafttracker/common/Constants.java b/src/main/java/com/sweetrpg/crafttracker/common/Constants.java index c0397a7..397ad8e 100644 --- a/src/main/java/com/sweetrpg/crafttracker/common/Constants.java +++ b/src/main/java/com/sweetrpg/crafttracker/common/Constants.java @@ -45,6 +45,8 @@ public class Constants { public static final String TRANSLATION_KEY_GUI_QUEUE_MGR_DEC_BUTTON_TOOLTIP = "crafttracker.screen.queue_mgr.button.dec.tooltip"; public static final String TRANSLATION_KEY_GUI_QUEUE_MGR_INC_BUTTON_TOOLTIP = "crafttracker.screen.queue_mgr.button.inc.tooltip"; public static final String TRANSLATION_KEY_GUI_QUEUE_MGR_DEL_BUTTON_TOOLTIP = "crafttracker.screen.queue_mgr.button.del.tooltip"; + public static final String TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_TITLE = "crafttracker.screen.queue_mgr.picker.title"; + public static final String TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_SEARCH = "crafttracker.screen.queue_mgr.picker.search_label"; public static final String TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE = "crafttracker.msg.queue_overlay_mode.hide"; public static final String TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW = "crafttracker.msg.queue_overlay_mode.show"; public static final String TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC = "crafttracker.msg.queue_overlay_mode.dynamic"; diff --git a/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java b/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java index 1d69251..da1dbf7 100644 --- a/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java +++ b/src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java @@ -47,6 +47,8 @@ private void processENUS() { add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_DEC_BUTTON_TOOLTIP, "Decrease the amount crafted"); add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_INC_BUTTON_TOOLTIP, "Increase the amount crafted"); add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_DEL_BUTTON_TOOLTIP, "Delete this item from the queue"); + add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_TITLE, "Add Item"); + add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_SEARCH, "Search:"); add(Constants.KEY_BINDINGS_CATEGORY_TITLE, "Craft Tracker"); add(Constants.TRANSLATION_KEY_BINDINGS_ADDTOQUEUE_TITLE, "Add to Queue"); add(Constants.TRANSLATION_KEY_BINDINGS_TOGGLE_CRAFT_QUEUE_TITLE, "Toggle Craft Queue"); @@ -106,6 +108,8 @@ private void processENGB() { add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_DEC_BUTTON_TOOLTIP, "Decrease the amount crafted"); add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_INC_BUTTON_TOOLTIP, "Increase the amount crafted"); add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_DEL_BUTTON_TOOLTIP, "Delete this item from the queue"); + add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_TITLE, "Add Item"); + add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_SEARCH, "Search:"); add(Constants.KEY_BINDINGS_CATEGORY_TITLE, "Craft Tracker"); add(Constants.TRANSLATION_KEY_BINDINGS_ADDTOQUEUE_TITLE, "Add to Queue"); add(Constants.TRANSLATION_KEY_BINDINGS_TOGGLE_CRAFT_QUEUE_TITLE, "Toggle Craft Queue"); @@ -165,6 +169,8 @@ private void processDEDE() { add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_DEC_BUTTON_TOOLTIP, "Reduzieren Sie die Menge an hergestellten"); add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_INC_BUTTON_TOOLTIP, "Erhöhen Sie die Menge an hergestellten"); add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_DEL_BUTTON_TOOLTIP, "Dieses Element aus der Warteschlange löschen"); + add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_TITLE, "Gegenstand hinzufügen"); + add(Constants.TRANSLATION_KEY_GUI_QUEUE_MGR_PICKER_SEARCH, "Suchen:"); add(Constants.KEY_BINDINGS_CATEGORY_TITLE, "Handwerks-Tracker"); add(Constants.TRANSLATION_KEY_BINDINGS_ADDTOQUEUE_TITLE, "Zur Warteschlange hinzufügen"); add(Constants.TRANSLATION_KEY_BINDINGS_TOGGLE_CRAFT_QUEUE_TITLE, "Herstellungswarteschlange umschalten"); diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/HoverItemProvider.java b/src/main/java/com/sweetrpg/crafttracker/integration/HoverItemProvider.java new file mode 100644 index 0000000..39ee504 --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/HoverItemProvider.java @@ -0,0 +1,22 @@ +package com.sweetrpg.crafttracker.integration; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.ResourceLocation; + +import java.util.Optional; + +/** + * Resolves the item under the mouse for the current screen. + * Implementations are registered in {@link HoverProviderRegistry} in priority order. + */ +public interface HoverItemProvider { + + /** + * Higher value = queried first. + */ + int getPriority(); + + String getName(); + + Optional getItemUnderMouse(Screen screen); +} diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/HoverProviderRegistry.java b/src/main/java/com/sweetrpg/crafttracker/integration/HoverProviderRegistry.java new file mode 100644 index 0000000..6e54afe --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/HoverProviderRegistry.java @@ -0,0 +1,33 @@ +package com.sweetrpg.crafttracker.integration; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +/** + * Holds registered {@link HoverItemProvider}s in descending priority order. + * Query with {@link #resolve(Screen)} to get the first non-empty result. + */ +public class HoverProviderRegistry { + + private static final List PROVIDERS = new ArrayList<>(); + + public static void register(HoverItemProvider provider) { + PROVIDERS.add(provider); + PROVIDERS.sort(Comparator.comparingInt(HoverItemProvider::getPriority).reversed()); + } + + public static Optional resolve(Screen screen) { + for (HoverItemProvider provider : PROVIDERS) { + Optional result = provider.getItemUnderMouse(screen); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/VanillaSlotHoverProvider.java b/src/main/java/com/sweetrpg/crafttracker/integration/VanillaSlotHoverProvider.java new file mode 100644 index 0000000..3b466fa --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/VanillaSlotHoverProvider.java @@ -0,0 +1,36 @@ +package com.sweetrpg.crafttracker.integration; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.resources.ResourceLocation; + +import java.util.Optional; + +/** + * Resolves the item under the mouse by reading + * {@link AbstractContainerScreen#hoveredSlot}. Always registered. + */ +public class VanillaSlotHoverProvider implements HoverItemProvider { + + @Override + public int getPriority() { + return 0; + } + + @Override + public String getName() { + return "vanilla-slot"; + } + + @Override + public Optional getItemUnderMouse(Screen screen) { + if (!(screen instanceof AbstractContainerScreen containerScreen)) { + return Optional.empty(); + } + var slot = containerScreen.hoveredSlot; + if (slot == null || !slot.hasItem()) { + return Optional.empty(); + } + return Optional.ofNullable(slot.getItem().getItem().getRegistryName()); + } +} 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 7e46112..106a9d2 100644 --- a/src/main/java/com/sweetrpg/crafttracker/integration/jei/CTPlugin.java +++ b/src/main/java/com/sweetrpg/crafttracker/integration/jei/CTPlugin.java @@ -2,23 +2,16 @@ import com.sweetrpg.crafttracker.CraftTracker; import com.sweetrpg.crafttracker.common.Constants; -import com.sweetrpg.crafttracker.common.manager.CraftingQueueManager; -import com.sweetrpg.crafttracker.common.manager.ShoppingListManager; +import com.sweetrpg.crafttracker.integration.HoverProviderRegistry; import mezz.jei.api.IModPlugin; import mezz.jei.api.JeiPlugin; import mezz.jei.api.registration.*; import mezz.jei.api.runtime.IJeiRuntime; -import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Player; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.fml.DistExecutor; @JeiPlugin public class CTPlugin implements IModPlugin { - public static IJeiRuntime jeiRuntime; - @Override public ResourceLocation getPluginUid() { return new ResourceLocation(Constants.MOD_ID, Constants.JEI_PLUGIN_ID); @@ -81,14 +74,6 @@ public void registerAdvanced(IAdvancedRegistration registration) { @Override public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { CraftTracker.LOGGER.debug("CTPlugin#onRuntimeAvailable: {}", jeiRuntime); - - CTPlugin.jeiRuntime = jeiRuntime; - - // TODO: move this elsewhere to remove hard dependency on JEI - DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { - Player player = Minecraft.getInstance().player; - CraftingQueueManager.INSTANCE.load(player); - ShoppingListManager.INSTANCE.load(player); - }); + HoverProviderRegistry.register(new JeiHoverProvider(jeiRuntime)); } } diff --git a/src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiHoverProvider.java b/src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiHoverProvider.java new file mode 100644 index 0000000..eac80eb --- /dev/null +++ b/src/main/java/com/sweetrpg/crafttracker/integration/jei/JeiHoverProvider.java @@ -0,0 +1,40 @@ +package com.sweetrpg.crafttracker.integration.jei; + +import com.sweetrpg.crafttracker.integration.HoverItemProvider; +import mezz.jei.api.runtime.IJeiRuntime; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import java.util.Optional; + +/** + * Resolves the item under the mouse via the JEI ingredient-list overlay. + * Only registered when JEI is loaded; only this class and {@link CTPlugin} + * may import JEI API types. + */ +public class JeiHoverProvider implements HoverItemProvider { + + private final IJeiRuntime jeiRuntime; + + public JeiHoverProvider(IJeiRuntime jeiRuntime) { + this.jeiRuntime = jeiRuntime; + } + + @Override + public int getPriority() { + return 100; + } + + @Override + public String getName() { + return "jei"; + } + + @Override + public Optional getItemUnderMouse(Screen screen) { + return jeiRuntime.getIngredientListOverlay().getIngredientUnderMouse() + .filter(i -> i.getIngredient() instanceof ItemStack) + .map(i -> ((ItemStack) i.getIngredient()).getItem().getRegistryName()); + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index b772753..0967d3f 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -1,6 +1,9 @@ # Screen public net.minecraft.client.gui.screens.Screen f_96545_ # buttons +# AbstractContainerScreen +public net.minecraft.client.gui.screens.inventory.AbstractContainerScreen f_97734_ # hoveredSlot + # RecipeManager public net.minecraft.world.item.crafting.RecipeManager m_44054_(Lnet/minecraft/world/item/crafting/RecipeType;)Ljava/util/Map; # byType diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index c24d51f..28b8d83 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -32,7 +32,7 @@ displayName = "Craft Tracker" [[dependencies.crafttracker]] modId="jei" - mandatory=true +mandatory = false versionRange="[9.7.2.281,)" ordering="NONE" side="BOTH" From 0c813f3ad7440977147ea65edc3cd53f18f2e369 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 07:00:43 -0700 Subject: [PATCH 02/18] feat: remove JEI hard dependency Add hover providers for vanilla and JEI to get item information Add item picker text box to queue manager Refs: https://github.com/sweetrpg/CraftTracker/issues/97 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9e497e3..21fed89 100644 --- a/build.gradle +++ b/build.gradle @@ -204,7 +204,7 @@ curseforge { addGameVersion project.target_mc_version relations { optionalDependency 'configured' - requiredDependency 'jei' + optionalDependency 'jei' optionalDependency 'patchouli' } } @@ -224,7 +224,7 @@ modrinth { loaders = ["forge"] dependencies { optional.project "configured" - required.project "jei" + optional.project "jei" optional.project "patchouli" } } From 630f0a0ed1d044b4914fdff51f75de37d94004b4 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 07:44:35 -0700 Subject: [PATCH 03/18] chore: update local Java version --- .idea/jarRepositories.xml | 50 +++++++++++++++++++++++++++++++++++++++ .java-version | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .idea/jarRepositories.xml diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..5bdc2e9 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.java-version b/.java-version index 6215fcb..dbb360e 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17.0.13 +17.0.19 From 744472a8da068c46f837776aa5de9d5b26469aee Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 07:47:43 -0700 Subject: [PATCH 04/18] chore: update release notes --- CHANGELOG/1.18/current.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG/1.18/current.md b/CHANGELOG/1.18/current.md index e69de29..77dcc74 100644 --- a/CHANGELOG/1.18/current.md +++ b/CHANGELOG/1.18/current.md @@ -0,0 +1 @@ +- [NEW] JEI is now an optional dependency From b1b5e7fe16b7ce1eeb5e6d17f2a4b18b541b3adc Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 07:48:36 -0700 Subject: [PATCH 05/18] chore: update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3650f34..bd41bff 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ out *.iws *.iml .idea/workspace.xml +.idea/shelf/ # gradle build From 1e15e339d6225618b16443c295531f3b6ae72ae1 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 07:58:53 -0700 Subject: [PATCH 06/18] chore: comment out JEI runtimeOnly for no-JEI dev testing Facilitates manual verification that the mod runs cleanly without JEI installed (task 7.3). Restore this line after testing is complete. Co-Authored-By: Claude Sonnet 4.6 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 21fed89..f2c01d5 100644 --- a/build.gradle +++ b/build.gradle @@ -120,7 +120,7 @@ dependencies { minecraft "net.minecraftforge:forge:${project.forge_version}" compileOnly fg.deobf("mezz.jei:jei-${jei_version}:api") - runtimeOnly fg.deobf("mezz.jei:jei-${jei_version}") + // runtimeOnly fg.deobf("mezz.jei:jei-${jei_version}") compileOnly fg.deobf("vazkii.patchouli:Patchouli:${ptch_version}:api") runtimeOnly fg.deobf("vazkii.patchouli:Patchouli:${ptch_version}") runtimeOnly fg.deobf("curse.maven:the-one-probe-${top_project_id}:${top_file_id}") From ff11fd16355689921efa80ff3f86e14465434649 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 08:22:31 -0700 Subject: [PATCH 07/18] chore: restore JEI runtime dependency in build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f2c01d5..21fed89 100644 --- a/build.gradle +++ b/build.gradle @@ -120,7 +120,7 @@ dependencies { minecraft "net.minecraftforge:forge:${project.forge_version}" compileOnly fg.deobf("mezz.jei:jei-${jei_version}:api") - // runtimeOnly fg.deobf("mezz.jei:jei-${jei_version}") + runtimeOnly fg.deobf("mezz.jei:jei-${jei_version}") compileOnly fg.deobf("vazkii.patchouli:Patchouli:${ptch_version}:api") runtimeOnly fg.deobf("vazkii.patchouli:Patchouli:${ptch_version}") runtimeOnly fg.deobf("curse.maven:the-one-probe-${top_project_id}:${top_file_id}") From 8c9242e1fe152b812e0dc588d05d1f2287cf8c7e Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 11:05:50 -0700 Subject: [PATCH 08/18] chore: update Gradle version to 8.14.5 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a595206..b413873 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 516df576da8ccfc51a32dfe2d618d3e3a286b6c9 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 11:06:09 -0700 Subject: [PATCH 09/18] chore: remove blank line --- src/main/java/com/sweetrpg/crafttracker/CraftTracker.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/sweetrpg/crafttracker/CraftTracker.java b/src/main/java/com/sweetrpg/crafttracker/CraftTracker.java index ec58732..a40f7c3 100644 --- a/src/main/java/com/sweetrpg/crafttracker/CraftTracker.java +++ b/src/main/java/com/sweetrpg/crafttracker/CraftTracker.java @@ -82,7 +82,6 @@ public void clientSetup(final FMLClientSetupEvent event) { LOGGER.debug("Client startup"); ClientSetup.setupScreenManagers(event); - } protected void interModProcess(final InterModProcessEvent event) { From e572c1afb9f58bc717dc3d7d207692a16b4f266d Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 11:16:51 -0700 Subject: [PATCH 10/18] chore: remove unnecessary quotes --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 21fed89..b2c4ec8 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,9 @@ minecraft { property 'forge.logging.console.level', 'debug' property 'mixin.env.remapRefMap', 'true' property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" - args '--mod', project.mod_id, '--all', '--output', '"' + file('src/generated/resources/') + '"', '--existing', '"' + sourceSets.main.resources.srcDirs[0] + '"' + args '--mod', project.mod_id, '--all', + '--output', file('src/generated/resources/'), + '--existing', sourceSets.main.resources.srcDirs[0] mods { create(mod_id) { source(sourceSets.main) From 1aa4020769f76638180ecf4ec90574b2fa20ea92 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 11:17:01 -0700 Subject: [PATCH 11/18] chore: updated language files --- src/generated/resources/assets/crafttracker/lang/de_de.json | 2 ++ src/generated/resources/assets/crafttracker/lang/en_gb.json | 2 ++ src/generated/resources/assets/crafttracker/lang/en_us.json | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/generated/resources/assets/crafttracker/lang/de_de.json b/src/generated/resources/assets/crafttracker/lang/de_de.json index 392877b..1a29745 100644 --- a/src/generated/resources/assets/crafttracker/lang/de_de.json +++ b/src/generated/resources/assets/crafttracker/lang/de_de.json @@ -40,6 +40,8 @@ "crafttracker.screen.queue_mgr.button.dec.tooltip": "Reduzieren Sie die Menge an hergestellten", "crafttracker.screen.queue_mgr.button.del.tooltip": "Dieses Element aus der Warteschlange l\u00F6schen", "crafttracker.screen.queue_mgr.button.inc.tooltip": "Erh\u00F6hen Sie die Menge an hergestellten", + "crafttracker.screen.queue_mgr.picker.search_label": "Suchen:", + "crafttracker.screen.queue_mgr.picker.title": "Gegenstand hinzuf\u00FCgen", "crafttracker.screen.queue_mgr.title": "Warteschlangenmanager", "crafttracker.screen.shopping_list.empty_message": "The shopping list is empty.", "crafttracker.screen.shopping_list.help_message": "To clear the list:", diff --git a/src/generated/resources/assets/crafttracker/lang/en_gb.json b/src/generated/resources/assets/crafttracker/lang/en_gb.json index 38a443d..9f9d73d 100644 --- a/src/generated/resources/assets/crafttracker/lang/en_gb.json +++ b/src/generated/resources/assets/crafttracker/lang/en_gb.json @@ -40,6 +40,8 @@ "crafttracker.screen.queue_mgr.button.dec.tooltip": "Decrease the amount crafted", "crafttracker.screen.queue_mgr.button.del.tooltip": "Delete this item from the queue", "crafttracker.screen.queue_mgr.button.inc.tooltip": "Increase the amount crafted", + "crafttracker.screen.queue_mgr.picker.search_label": "Search:", + "crafttracker.screen.queue_mgr.picker.title": "Add Item", "crafttracker.screen.queue_mgr.title": "Queue Manager", "crafttracker.screen.shopping_list.empty_message": "The shopping list is empty.", "crafttracker.screen.shopping_list.help_message": "To clear the list:", diff --git a/src/generated/resources/assets/crafttracker/lang/en_us.json b/src/generated/resources/assets/crafttracker/lang/en_us.json index 38a443d..9f9d73d 100644 --- a/src/generated/resources/assets/crafttracker/lang/en_us.json +++ b/src/generated/resources/assets/crafttracker/lang/en_us.json @@ -40,6 +40,8 @@ "crafttracker.screen.queue_mgr.button.dec.tooltip": "Decrease the amount crafted", "crafttracker.screen.queue_mgr.button.del.tooltip": "Delete this item from the queue", "crafttracker.screen.queue_mgr.button.inc.tooltip": "Increase the amount crafted", + "crafttracker.screen.queue_mgr.picker.search_label": "Search:", + "crafttracker.screen.queue_mgr.picker.title": "Add Item", "crafttracker.screen.queue_mgr.title": "Queue Manager", "crafttracker.screen.shopping_list.empty_message": "The shopping list is empty.", "crafttracker.screen.shopping_list.help_message": "To clear the list:", From 104110bc1cd7588390825860d8c517101afaf410 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 11:40:15 -0700 Subject: [PATCH 12/18] chore: update tasks --- openspec/changes/remove-jei-hard-dependency/tasks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openspec/changes/remove-jei-hard-dependency/tasks.md b/openspec/changes/remove-jei-hard-dependency/tasks.md index 41a0065..5879b7e 100644 --- a/openspec/changes/remove-jei-hard-dependency/tasks.md +++ b/openspec/changes/remove-jei-hard-dependency/tasks.md @@ -47,5 +47,5 @@ ## 7. Verification - [x] 7.1 `./gradlew build` and `./gradlew test` pass -- [ ] 7.2 Launch `./gradlew client` with JEI present: verify hover-add from container slots, hover-add from the JEI panel, the picker, and that queue/list load on join -- [ ] 7.3 Launch the client with JEI removed from `runtimeOnly`: verify the mod loads (no `NoClassDefFoundError`), hover-add from slots works, the picker works, and queue/list load on join +- [x] 7.2 Launch `./gradlew client` with JEI present: verify hover-add from container slots, hover-add from the JEI panel, the picker, and that queue/list load on join +- [x] 7.3 Launch the client with JEI removed from `runtimeOnly`: verify the mod loads (no `NoClassDefFoundError`), hover-add from slots works, the picker works, and queue/list load on join From 683eb5e8d14360cb248f7afb6d6bce16480922ca Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 11:42:15 -0700 Subject: [PATCH 13/18] chore: archive completed change --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/hover-provider-integration/spec.md | 0 .../specs/queue-data-lifecycle/spec.md | 0 .../specs/queue-item-input/spec.md | 0 .../tasks.md | 0 .../specs/hover-provider-integration/spec.md | 42 +++++++++++++++++ openspec/specs/queue-data-lifecycle/spec.md | 31 +++++++++++++ openspec/specs/queue-item-input/spec.md | 46 +++++++++++++++++++ 10 files changed, 119 insertions(+) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/.openspec.yaml (100%) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/design.md (100%) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/proposal.md (100%) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/specs/hover-provider-integration/spec.md (100%) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/specs/queue-data-lifecycle/spec.md (100%) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/specs/queue-item-input/spec.md (100%) rename openspec/changes/{remove-jei-hard-dependency => archive/2026-06-21-remove-jei-hard-dependency}/tasks.md (100%) create mode 100644 openspec/specs/hover-provider-integration/spec.md create mode 100644 openspec/specs/queue-data-lifecycle/spec.md create mode 100644 openspec/specs/queue-item-input/spec.md diff --git a/openspec/changes/remove-jei-hard-dependency/.openspec.yaml b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/.openspec.yaml similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/.openspec.yaml rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/.openspec.yaml diff --git a/openspec/changes/remove-jei-hard-dependency/design.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/design.md similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/design.md rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/design.md diff --git a/openspec/changes/remove-jei-hard-dependency/proposal.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/proposal.md similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/proposal.md rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/proposal.md diff --git a/openspec/changes/remove-jei-hard-dependency/specs/hover-provider-integration/spec.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/specs/hover-provider-integration/spec.md similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/specs/hover-provider-integration/spec.md rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/specs/hover-provider-integration/spec.md diff --git a/openspec/changes/remove-jei-hard-dependency/specs/queue-data-lifecycle/spec.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/specs/queue-data-lifecycle/spec.md similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/specs/queue-data-lifecycle/spec.md rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/specs/queue-data-lifecycle/spec.md diff --git a/openspec/changes/remove-jei-hard-dependency/specs/queue-item-input/spec.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/specs/queue-item-input/spec.md similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/specs/queue-item-input/spec.md rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/specs/queue-item-input/spec.md diff --git a/openspec/changes/remove-jei-hard-dependency/tasks.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/tasks.md similarity index 100% rename from openspec/changes/remove-jei-hard-dependency/tasks.md rename to openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/tasks.md diff --git a/openspec/specs/hover-provider-integration/spec.md b/openspec/specs/hover-provider-integration/spec.md new file mode 100644 index 0000000..ed62719 --- /dev/null +++ b/openspec/specs/hover-provider-integration/spec.md @@ -0,0 +1,42 @@ +# hover-provider-integration Specification + +## Purpose +TBD - created by archiving change remove-jei-hard-dependency. Update Purpose after archive. +## Requirements +### Requirement: Pluggable item-under-mouse providers + +The mod SHALL resolve "the item under the mouse" through a provider abstraction rather than a +direct JEI call. A native provider backed by the vanilla container slot SHALL always be +available. Optional providers backed by other mods (e.g. JEI, and in future REI) SHALL be +available only when the corresponding mod is loaded. + +#### Scenario: Native provider always present + +- **WHEN** the add-to-queue action runs and no optional item-list mod is loaded +- **THEN** the native container-slot provider resolves the hovered item + +#### Scenario: Optional provider used when its mod is present + +- **WHEN** JEI is loaded and the player hovers an item in JEI's ingredient list overlay +- **THEN** the JEI provider resolves that item, including items the player does not currently possess + +#### Scenario: Provider selection order + +- **WHEN** more than one provider could resolve an item under the mouse +- **THEN** the mod queries providers in a defined order and uses the first that returns an item + +### Requirement: JEI API code is isolated from no-JEI runtimes + +All code that references JEI API types SHALL be confined to classes that are loaded only when +JEI is present, so that running without JEI does not cause class-loading failures. + +#### Scenario: Running without JEI does not load JEI classes + +- **WHEN** the game starts with Craft Tracker installed but JEI absent +- **THEN** no JEI API class is loaded and the mod initializes without `NoClassDefFoundError` + +#### Scenario: Guarded access to the JEI provider + +- **WHEN** the mod decides whether to register the JEI hover provider +- **THEN** it checks that JEI is loaded before referencing any JEI-backed class + diff --git a/openspec/specs/queue-data-lifecycle/spec.md b/openspec/specs/queue-data-lifecycle/spec.md new file mode 100644 index 0000000..db23473 --- /dev/null +++ b/openspec/specs/queue-data-lifecycle/spec.md @@ -0,0 +1,31 @@ +# queue-data-lifecycle Specification + +## Purpose +TBD - created by archiving change remove-jei-hard-dependency. Update Purpose after archive. +## Requirements +### Requirement: Queue and shopping list load independent of JEI + +The crafting queue and shopping list SHALL be loaded for the client player on a JEI-independent +client lifecycle event (e.g. client login / world join), not from JEI's runtime-available +callback. Persisted data SHALL load whether or not JEI is installed. + +#### Scenario: Joining a world without JEI + +- **WHEN** the player joins a world with JEI absent +- **THEN** the crafting queue and shopping list are loaded from disk and reflect their previously saved state + +#### Scenario: Joining a world with JEI present + +- **WHEN** the player joins a world with JEI installed +- **THEN** the crafting queue and shopping list are loaded exactly once, with no dependence on JEI's runtime callback + +### Requirement: JEI dependency is optional + +The mod's dependency metadata SHALL declare JEI as optional rather than mandatory, and the mod +SHALL load successfully when JEI is not installed. + +#### Scenario: Loading with JEI absent + +- **WHEN** the game loads Craft Tracker without JEI installed +- **THEN** the mod loads without a missing-dependency error and all non-JEI features are available + diff --git a/openspec/specs/queue-item-input/spec.md b/openspec/specs/queue-item-input/spec.md new file mode 100644 index 0000000..8bcd7a6 --- /dev/null +++ b/openspec/specs/queue-item-input/spec.md @@ -0,0 +1,46 @@ +# queue-item-input Specification + +## Purpose +TBD - created by archiving change remove-jei-hard-dependency. Update Purpose after archive. +## Requirements +### Requirement: Add hovered item to queue from any container view + +The mod SHALL add the item currently under the mouse to the crafting queue when the +add-to-queue key is pressed while a vanilla container screen is open, without requiring JEI. +The hovered item SHALL be resolved from the container slot under the mouse so the action works +in the inventory, crafting table, and other container views. + +#### Scenario: Hovering an occupied slot in the inventory + +- **WHEN** the player opens the inventory, hovers a slot containing an item, and presses the add-to-queue key +- **THEN** that item is added to the crafting queue with quantity 1 +- **AND** the queue-item advancement is granted + +#### Scenario: Hovering an empty slot + +- **WHEN** the player presses the add-to-queue key while the slot under the mouse is empty +- **THEN** no item is added to the queue and no error is shown + +#### Scenario: JEI is not installed + +- **WHEN** JEI is absent and the player hovers an item in a container view and presses the add-to-queue key +- **THEN** the item is added to the queue using the native container slot, exactly as it would with JEI installed + +### Requirement: Add an item to the queue directly from the Queue Management screen + +The Queue Management screen SHALL let the player find any registered item by searching and/or +browsing and add it to the crafting queue directly, without needing to hover it in a container +or in JEI. This provides the full-registry add capability when the player does not currently +possess the item. + +#### Scenario: Searching for and adding an item + +- **WHEN** the player opens the Queue Management screen, enters a search term, and selects a matching item +- **THEN** that item is added to the crafting queue +- **AND** the queue recomputes its intermediates, raw materials, and fuel + +#### Scenario: Adding an item the player does not own + +- **WHEN** the player selects an item that is not present in any of their containers +- **THEN** the item is still added to the queue, since the picker browses the item registry rather than the player's inventory + From 2285875b474c715a1c1ea36c15fb962718d76015 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 17:43:25 -0700 Subject: [PATCH 14/18] ci: update gradle setup --- .github/workflows/pr-build.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index c359c48..0e7912b 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -16,16 +16,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version-file: .java-version distribution: 'temurin' + - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 + with: + gradle-version: 'wrapper' + - name: Build and test run: | gradle build test --console=verbose From 8390601a65b6aaa6263eb071f440a69568917279 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 17:47:11 -0700 Subject: [PATCH 15/18] chore: use .yaml extension --- .github/{FUNDING.yml => FUNDING.yaml} | 0 .github/{dependabot.yml => dependabot.yaml} | 0 .github/workflows/{ci-build.yml => ci-build.yaml} | 0 .../{codeql-analysis.yml => codeql-analysis.yaml} | 0 .github/workflows/{debug.yml => debug.yaml} | 0 .../{pr-build-release.yml => pr-build-release.yaml} | 11 +++++++++-- .github/workflows/{pr-build.yml => pr-build.yaml} | 1 + .github/workflows/{release.yml => release.yaml} | 0 8 files changed, 10 insertions(+), 2 deletions(-) rename .github/{FUNDING.yml => FUNDING.yaml} (100%) rename .github/{dependabot.yml => dependabot.yaml} (100%) rename .github/workflows/{ci-build.yml => ci-build.yaml} (100%) rename .github/workflows/{codeql-analysis.yml => codeql-analysis.yaml} (100%) rename .github/workflows/{debug.yml => debug.yaml} (100%) rename .github/workflows/{pr-build-release.yml => pr-build-release.yaml} (75%) rename .github/workflows/{pr-build.yml => pr-build.yaml} (94%) rename .github/workflows/{release.yml => release.yaml} (100%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yaml similarity index 100% rename from .github/FUNDING.yml rename to .github/FUNDING.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yaml similarity index 100% rename from .github/dependabot.yml rename to .github/dependabot.yaml diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yaml similarity index 100% rename from .github/workflows/ci-build.yml rename to .github/workflows/ci-build.yaml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yaml similarity index 100% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql-analysis.yaml diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yaml similarity index 100% rename from .github/workflows/debug.yml rename to .github/workflows/debug.yaml diff --git a/.github/workflows/pr-build-release.yml b/.github/workflows/pr-build-release.yaml similarity index 75% rename from .github/workflows/pr-build-release.yml rename to .github/workflows/pr-build-release.yaml index 716494e..941277c 100644 --- a/.github/workflows/pr-build-release.yml +++ b/.github/workflows/pr-build-release.yaml @@ -9,6 +9,7 @@ on: - build.gradle - gradle.properties - settings.gradle + - .github/workflows/pr-build-release.yaml jobs: build: @@ -18,14 +19,20 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v6 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version-file: .java-version distribution: 'temurin' + - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: 'wrapper' + - name: Build and test run: | ./gradlew build test --console=verbose diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yaml similarity index 94% rename from .github/workflows/pr-build.yml rename to .github/workflows/pr-build.yaml index 0e7912b..0dd7a3a 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yaml @@ -9,6 +9,7 @@ on: - build.gradle - gradle.properties - settings.gradle + - .github/workflows/pr-build.yaml jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yaml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/release.yaml From 269c290913db8a6e99680e2d187d6b80095a858d Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 17:48:41 -0700 Subject: [PATCH 16/18] chore: use gradle actions setup-gradle v5 --- .github/workflows/pr-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 0dd7a3a..552a5a3 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -29,7 +29,7 @@ jobs: distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 + uses: gradle/actions/setup-gradle@v5 with: gradle-version: 'wrapper' From bbd3734565081bcd88fa87aa1978066d71f999f6 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 17:50:04 -0700 Subject: [PATCH 17/18] chore: pin gradle version to 8.14.5 --- .github/workflows/pr-build-release.yaml | 2 +- .github/workflows/pr-build.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-build-release.yaml b/.github/workflows/pr-build-release.yaml index 941277c..dedf93a 100644 --- a/.github/workflows/pr-build-release.yaml +++ b/.github/workflows/pr-build-release.yaml @@ -31,7 +31,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: - gradle-version: 'wrapper' + gradle-version: 8.14.5 - name: Build and test run: | diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 552a5a3..522c1d6 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -31,7 +31,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: - gradle-version: 'wrapper' + gradle-version: 8.14.5 - name: Build and test run: | From 5697e6be4f7e6beb547b8e1f5a2856fcfc9c0ab2 Mon Sep 17 00:00:00 2001 From: Paul Schifferer Date: Sun, 21 Jun 2026 17:53:22 -0700 Subject: [PATCH 18/18] chore: set fetch depth and update java action step --- .github/workflows/pr-build-release.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-build-release.yaml b/.github/workflows/pr-build-release.yaml index dedf93a..790acdd 100644 --- a/.github/workflows/pr-build-release.yaml +++ b/.github/workflows/pr-build-release.yaml @@ -21,9 +21,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version-file: .java-version distribution: 'temurin'