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 68%
rename from .github/workflows/pr-build-release.yml
rename to .github/workflows/pr-build-release.yaml
index 716494e..790acdd 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,22 @@ jobs:
contents: write
steps:
- - uses: actions/checkout@v4
+ - 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'
+
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@v5
+ with:
+ gradle-version: 8.14.5
+
- 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 70%
rename from .github/workflows/pr-build.yml
rename to .github/workflows/pr-build.yaml
index c359c48..522c1d6 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:
@@ -16,16 +17,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@v5
+ with:
+ gradle-version: 8.14.5
+
- name: Build and test
run: |
gradle build test --console=verbose
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
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
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
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
diff --git a/build.gradle b/build.gradle
index 9e497e3..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)
@@ -204,7 +206,7 @@ curseforge {
addGameVersion project.target_mc_version
relations {
optionalDependency 'configured'
- requiredDependency 'jei'
+ optionalDependency 'jei'
optionalDependency 'patchouli'
}
}
@@ -224,7 +226,7 @@ modrinth {
loaders = ["forge"]
dependencies {
optional.project "configured"
- required.project "jei"
+ optional.project "jei"
optional.project "patchouli"
}
}
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
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/archive/2026-06-21-remove-jei-hard-dependency/tasks.md b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/tasks.md
new file mode 100644
index 0000000..5879b7e
--- /dev/null
+++ b/openspec/changes/archive/2026-06-21-remove-jei-hard-dependency/tasks.md
@@ -0,0 +1,51 @@
+## 1. Hover provider abstraction
+
+- [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
+
+- [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
+
+- [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
+
+- [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
+
+- [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
+
+- [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
+
+- [x] 7.1 `./gradlew build` and `./gradlew test` pass
+- [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
diff --git a/openspec/changes/remove-jei-hard-dependency/tasks.md b/openspec/changes/remove-jei-hard-dependency/tasks.md
deleted file mode 100644
index 08ee42c..0000000
--- a/openspec/changes/remove-jei-hard-dependency/tasks.md
+++ /dev/null
@@ -1,41 +0,0 @@
-## 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
-
-## 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
-
-## 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)
-
-## 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
-
-## 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`
-
-## 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)
-
-## 7. Verification
-
-- [ ] 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/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
+
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:",
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) {
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"