-
Notifications
You must be signed in to change notification settings - Fork 0
Add normal Workbench #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a91f5c4
4d00701
6c39d4c
b8c1841
5f162d4
13c40cb
a9e2d85
dfa2baa
bf2c444
6971c34
8819d40
030ccd2
91f60c7
d47ff42
5105790
ce734fd
185249b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,66 @@ | ||
| package com.tcm.MineTale; | ||
|
|
||
| import com.tcm.MineTale.block.workbenches.screen.FurnaceWorkbenchScreen; | ||
| import com.tcm.MineTale.block.workbenches.screen.WorkbenchWorkbenchScreen; | ||
| import com.tcm.MineTale.network.ClientboundNearbyInventorySyncPacket; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; | ||
| import com.tcm.MineTale.block.workbenches.screen.CampfireWorkbenchScreen; | ||
| import com.tcm.MineTale.registry.ModMenuTypes; | ||
|
|
||
| import net.fabricmc.api.ClientModInitializer; | ||
| import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; | ||
| import net.minecraft.client.gui.screens.MenuScreens; | ||
| import net.minecraft.client.gui.screens.Screen; | ||
| import net.minecraft.client.gui.screens.recipebook.RecipeUpdateListener; | ||
| import net.minecraft.world.item.ItemStack; | ||
|
|
||
| public class MineTaleClient implements ClientModInitializer { | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * Registers client-side screen factories for custom workbench menu types. | ||
| * Register client-side screen factories for custom workbench menu types. | ||
| * | ||
| * Binds the furnace and campfire workbench menu types to their corresponding screen constructors | ||
| * so the client can create the appropriate GUI when those menus are opened. | ||
| * Binds ModMenuTypes.FURNACE_WORKBENCH_MENU to FurnaceWorkbenchScreen, | ||
| * ModMenuTypes.CAMPFIRE_WORKBENCH_MENU to CampfireWorkbenchScreen, and | ||
| * ModMenuTypes.WORKBENCH_WORKBENCH_MENU to WorkbenchWorkbenchScreen so the client | ||
| * can create the appropriate GUI when those menus open. | ||
| */ | ||
| @Override | ||
| public void onInitializeClient() { | ||
| MenuScreens.register(ModMenuTypes.FURNACE_WORKBENCH_MENU, FurnaceWorkbenchScreen::new); | ||
| MenuScreens.register(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, CampfireWorkbenchScreen::new); | ||
| MenuScreens.register(ModMenuTypes.WORKBENCH_WORKBENCH_MENU, WorkbenchWorkbenchScreen::new); | ||
|
|
||
| ClientPlayNetworking.registerGlobalReceiver(ClientboundNearbyInventorySyncPacket.TYPE, (payload, context) -> { | ||
| List<ItemStack> items = payload.items(); | ||
|
|
||
| // We create a task that can re-run itself if the menu isn't ready yet | ||
| context.client().execute(new Runnable() { | ||
| int retries = 0; | ||
|
|
||
| @Override | ||
| public void run() { | ||
| if (context.client().player != null && context.client().player.containerMenu instanceof AbstractWorkbenchContainerMenu menu) { | ||
| applyItemsToMenu(menu, items, context.client().screen); | ||
| } else if (retries < 10) { // Try for up to 10 frames (~0.5 seconds) | ||
| retries++; | ||
| // Re-submit to the next tick | ||
| context.client().execute(this); | ||
| } else { | ||
| System.out.println("CLIENT: Failed to sync nearby items after 10 retries."); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| // Helper method to keep things clean | ||
| private static void applyItemsToMenu(AbstractWorkbenchContainerMenu menu, List<ItemStack> items, Screen screen) { | ||
| System.out.println("CLIENT: Successfully applied " + items.size() + " stacks to the Workbench Menu."); | ||
| menu.setNetworkedNearbyItems(items); | ||
| if (screen instanceof RecipeUpdateListener listener) { | ||
| listener.recipesUpdated(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,305 @@ | ||
| package com.tcm.MineTale.block.workbenches.screen; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import com.tcm.MineTale.MineTale; | ||
| import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; | ||
| import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; | ||
| import com.tcm.MineTale.block.workbenches.menu.WorkbenchWorkbenchMenu; | ||
| import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor; | ||
| import com.tcm.MineTale.mixin.client.RecipeBookComponentAccessor; | ||
| import com.tcm.MineTale.network.CraftRequestPayload; | ||
| import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; | ||
| import com.tcm.MineTale.recipe.WorkbenchRecipe; | ||
| import com.tcm.MineTale.registry.ModBlocks; | ||
| import com.tcm.MineTale.registry.ModRecipeDisplay; | ||
| import com.tcm.MineTale.registry.ModRecipes; | ||
|
|
||
| import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; | ||
| import net.minecraft.client.ClientRecipeBook; | ||
| import net.minecraft.client.gui.GuiGraphics; | ||
| import net.minecraft.client.gui.components.Button; | ||
| import net.minecraft.client.gui.navigation.ScreenPosition; | ||
| import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; | ||
| import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; | ||
| import net.minecraft.client.gui.screens.recipebook.RecipeCollection; | ||
| import net.minecraft.client.renderer.RenderPipelines; | ||
| import net.minecraft.core.HolderSet; | ||
| import net.minecraft.resources.Identifier; | ||
| import net.minecraft.server.level.ServerPlayer; | ||
| import net.minecraft.world.entity.player.Inventory; | ||
| import net.minecraft.world.entity.player.Player; | ||
| import net.minecraft.world.item.Item; | ||
| import net.minecraft.world.item.ItemStack; | ||
| import net.minecraft.world.item.crafting.Ingredient; | ||
| import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; | ||
| import net.minecraft.world.item.crafting.display.RecipeDisplayId; | ||
| import net.minecraft.world.item.crafting.display.SlotDisplayContext; | ||
| import net.minecraft.network.chat.Component; | ||
|
|
||
| public class WorkbenchWorkbenchScreen extends AbstractRecipeBookScreen<WorkbenchWorkbenchMenu> { | ||
| private static final Identifier TEXTURE = | ||
| Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png"); | ||
|
|
||
| private final MineTaleRecipeBookComponent mineTaleRecipeBook; | ||
|
|
||
| private Button craftOneBtn; | ||
| private Button craftTenBtn; | ||
| private Button craftAllBtn; | ||
|
|
||
| /** | ||
| * Initialize a workbench GUI screen using the provided container menu, player inventory, and title. | ||
| * | ||
| * @param menu the menu supplying slots and synchronized state for this screen | ||
| * @param inventory the player's inventory to display and interact with | ||
| * @param title the title component shown at the top of the screen | ||
| */ | ||
| public WorkbenchWorkbenchScreen(WorkbenchWorkbenchMenu menu, Inventory inventory, Component title) { | ||
| this(menu, inventory, title, createRecipeBookComponent(menu)); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a WorkbenchWorkbenchScreen bound to the given menu, player inventory, title, and recipe book component. | ||
| * | ||
| * @param menu the menu backing this screen | ||
| * @param inventory the player's inventory shown in the screen | ||
| * @param title the screen title component | ||
| * @param recipeBook the MineTaleRecipeBookComponent used to display and manage recipes in this screen | ||
| */ | ||
| private WorkbenchWorkbenchScreen(WorkbenchWorkbenchMenu menu, Inventory inventory, Component title, MineTaleRecipeBookComponent recipeBook) { | ||
| super(menu, recipeBook, inventory, title); | ||
| this.mineTaleRecipeBook = recipeBook; | ||
| } | ||
|
|
||
| /** | ||
| * Create a MineTaleRecipeBookComponent configured for the workbench screen. | ||
| * | ||
| * @param menu the workbench menu used to initialize the recipe book component | ||
| * @return a MineTaleRecipeBookComponent containing the workbench tab and associated recipe category | ||
| */ | ||
| private static MineTaleRecipeBookComponent createRecipeBookComponent(WorkbenchWorkbenchMenu menu) { | ||
| ItemStack tabIcon = new ItemStack(ModBlocks.WORKBENCH_WORKBENCH_BLOCK.asItem()); | ||
|
|
||
| List<RecipeBookComponent.TabInfo> tabs = List.of( | ||
| new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.WORKBENCH_SEARCH) | ||
| ); | ||
|
|
||
| return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.WORKBENCH_TYPE); | ||
| } | ||
|
|
||
| /** | ||
| * Configure the screen's GUI dimensions and initialize widgets. | ||
| * | ||
| * Sets the layout size (imageWidth = 176, imageHeight = 166), delegates remaining | ||
| * layout initialization to the superclass, and creates the three craft buttons | ||
| * ("1", "10", "All") wired to their respective handlers. | ||
| */ | ||
| @Override | ||
| protected void init() { | ||
| // Important: Set your GUI size before super.init() | ||
| this.imageWidth = 176; | ||
| this.imageHeight = 166; | ||
|
|
||
| super.init(); | ||
|
|
||
| int defaultLeft = this.leftPos + 90; | ||
| int defaultTop = this.topPos + 25; | ||
|
|
||
| this.craftOneBtn = addRenderableWidget(Button.builder(Component.literal("Craft"), (button) -> { | ||
| handleCraftRequest(1); | ||
| }).bounds(defaultLeft, defaultTop, 75, 20).build()); | ||
|
|
||
| this.craftTenBtn = addRenderableWidget(Button.builder(Component.literal("x10"), (button) -> { | ||
| handleCraftRequest(10); | ||
| }).bounds(defaultLeft, defaultTop + 22, 35, 20).build()); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| this.craftAllBtn = addRenderableWidget(Button.builder(Component.literal("All"), (button) -> { | ||
| handleCraftRequest(-1); // -1 represents "All" logic | ||
| }).bounds(defaultLeft + 40, defaultTop + 22, 35, 20).build()); | ||
| } | ||
|
|
||
| /** | ||
| * Sends a crafting request for the currently selected recipe in the integrated recipe book. | ||
| * | ||
| * Locates the last recipe collection and last selected recipe ID from the recipe book component, | ||
| * resolves the recipe's result item, and sends a CraftRequestPayload to the server containing that | ||
| * item and the requested amount. | ||
| * | ||
| * @param amount the quantity to craft; use -1 to request crafting of the full available stack ("All") | ||
| */ | ||
|
|
||
| private void handleCraftRequest(int amount) { | ||
| // 1. Cast the book component to the Accessor to get the selected data | ||
| RecipeBookComponentAccessor accessor = (RecipeBookComponentAccessor) this.mineTaleRecipeBook; | ||
|
|
||
| RecipeCollection collection = accessor.getLastRecipeCollection(); | ||
| RecipeDisplayId displayId = accessor.getLastRecipe(); | ||
|
|
||
| if (collection != null && displayId != null) { | ||
| // 2. Find the visual entry | ||
| for (RecipeDisplayEntry entry : collection.getSelectedRecipes(RecipeCollection.CraftableStatus.ANY)) { | ||
| if (entry.id().equals(displayId)) { | ||
| // 3. Resolve result for the packet | ||
| List<ItemStack> results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); | ||
|
|
||
| if (!results.isEmpty()) { | ||
| ItemStack resultStack = results.get(0); | ||
|
|
||
| // 4. LOG FOR DEBUGGING | ||
| System.out.println("Sending craft request for: " + resultStack + " amount: " + amount); | ||
|
|
||
| ClientPlayNetworking.send(new CraftRequestPayload(resultStack, amount)); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| } else { | ||
| System.out.println("Request failed: Collection or DisplayID is null!"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Draws the workbench GUI background texture at the screen's top-left corner. | ||
| * | ||
| * @param guiGraphics the graphics context used to draw GUI elements | ||
| * @param f partial tick time for interpolation | ||
| * @param i current mouse x coordinate relative to the window | ||
| * @param j current mouse y coordinate relative to the window | ||
| */ | ||
| protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { | ||
| int k = this.leftPos; | ||
| int l = this.topPos; | ||
| guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256); | ||
| } | ||
|
|
||
| @Override | ||
| public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { | ||
| renderBackground(graphics, mouseX, mouseY, delta); | ||
| super.render(graphics, mouseX, mouseY, delta); | ||
|
|
||
| // Get the ID of the recipe clicked in the ghost-book | ||
| RecipeDisplayId displayId = this.mineTaleRecipeBook.getSelectedRecipeId(); | ||
| RecipeDisplayEntry selectedEntry = null; | ||
|
|
||
| if (displayId != null && this.minecraft.level != null) { | ||
| ClientRecipeBook book = this.minecraft.player.getRecipeBook(); | ||
| // Accessing the known recipes via your Accessor | ||
| Map<RecipeDisplayId, RecipeDisplayEntry> knownRecipes = ((ClientRecipeBookAccessor) book).getKnown(); | ||
| selectedEntry = knownRecipes.get(displayId); | ||
| } | ||
|
|
||
| // 2. Button Activation Logic | ||
| if (selectedEntry != null) { | ||
| // We use the entry directly. It contains the 15 ingredients needed! | ||
| boolean canCraftOne = canCraft(this.minecraft.player, selectedEntry, 1); | ||
| boolean canCraftMoreThanOne = canCraft(this.minecraft.player, selectedEntry, 2); | ||
| boolean canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10); | ||
|
|
||
| this.craftOneBtn.active = canCraftOne; | ||
| this.craftTenBtn.active = canCraftTen; | ||
| this.craftAllBtn.active = canCraftMoreThanOne; | ||
| } else { | ||
| this.craftOneBtn.active = false; | ||
| this.craftTenBtn.active = false; | ||
| this.craftAllBtn.active = false; | ||
| } | ||
|
|
||
| renderTooltip(graphics, mouseX, mouseY); | ||
| } | ||
|
|
||
| private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) { | ||
| if (player == null || entry == null) return false; | ||
|
|
||
| Optional<List<Ingredient>> reqs = entry.craftingRequirements(); | ||
| if (reqs.isEmpty()) return false; | ||
|
|
||
| // 1. Group ingredients by their underlying Item HolderSet. | ||
| // Since Ingredient doesn't override hashCode, we use the values field directly | ||
| // or use a List of Holders as the key for stable hashing. | ||
| Map<HolderSet<Item>, Integer> aggregatedRequirements = new HashMap<>(); | ||
|
|
||
| // Helper map to get back to an Ingredient object for the final check | ||
| Map<HolderSet<Item>, Ingredient> holderToIngredient = new HashMap<>(); | ||
|
|
||
| for (Ingredient ing : reqs.get()) { | ||
| // Accessing the 'values' via a custom accessor or reflection if private, | ||
| // but based on your source, we can use the Ingredient object itself | ||
| // IF we use a helper that handles the hashing correctly. | ||
|
|
||
| // Strategy: Use the stream of holders as a List key (Lists have stable hashcodes) | ||
| HolderSet<Item> key = ing.items().collect(Collectors.collectingAndThen(Collectors.toList(), HolderSet::direct)); | ||
|
|
||
| aggregatedRequirements.put(key, aggregatedRequirements.getOrDefault(key, 0) + 1); | ||
| holderToIngredient.putIfAbsent(key, ing); | ||
| } | ||
|
|
||
| // 2. Check the player's inventory | ||
| Inventory inv = player.getInventory(); | ||
| for (Map.Entry<HolderSet<Item>, Integer> entryReq : aggregatedRequirements.entrySet()) { | ||
| int totalNeeded = entryReq.getValue() * craftCount; | ||
| Ingredient originalIng = holderToIngredient.get(entryReq.getKey()); | ||
|
|
||
| if (!hasIngredientAmount(inv, originalIng, totalNeeded)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
Comment on lines
+214
to
+252
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the WorkbenchWorkbenchScreen.java file and check if canCraft is called in render()
find . -name "WorkbenchWorkbenchScreen.java" -type fRepository: CodeMonkeysMods/MineTale Length of output: 157 🏁 Script executed: # Search for the render method and canCraft invocations in the file
rg -n "render\(|canCraft" --type=java -A 5 -B 2 | head -100Repository: CodeMonkeysMods/MineTale Length of output: 12497 🏁 Script executed: # Search for HolderSet class definition and its equals/hashCode implementation
rg -n "class HolderSet|equals|hashCode" --type=java -A 3 -B 1 | grep -A 5 "HolderSet" | head -150Repository: CodeMonkeysMods/MineTale Length of output: 839 🏁 Script executed: # Look for HolderSet.direct method
rg -n "direct\(" --type=java -B 3 -A 10 | grep -A 10 "HolderSet" | head -100Repository: CodeMonkeysMods/MineTale Length of output: 50 🏁 Script executed: # Find imports of HolderSet in the WorkbenchWorkbenchScreen file
rg -n "import.*HolderSet" src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.javaRepository: CodeMonkeysMods/MineTale Length of output: 107 🏁 Script executed: # Search for HolderSet class definition in entire codebase
rg -n "class HolderSet" --type=javaRepository: CodeMonkeysMods/MineTale Length of output: 50 🏁 Script executed: # Look for HolderSet.direct method definition
rg -n "static.*direct\(" --type=java -A 10 | grep -B 2 -A 10 "HolderSet"Repository: CodeMonkeysMods/MineTale Length of output: 50 🏁 Script executed: # Check if HolderSet equals/hashCode are defined anywhere in the codebase
fd -t f -name "*.java" -exec grep -l "HolderSet" {} \; | head -20Repository: CodeMonkeysMods/MineTale Length of output: 298 🏁 Script executed: # Find all Java files that reference HolderSet to understand its usage pattern
rg -l "HolderSet" --type=java | head -10Repository: CodeMonkeysMods/MineTale Length of output: 155 🏁 Script executed: # Check the actual usage of HolderSet.direct in the codebase
rg -n "HolderSet\.direct" --type=java -B 3 -A 3Repository: CodeMonkeysMods/MineTale Length of output: 50 🌐 Web query:
💡 Result: In older Minecraft versions (e.g., 1.18.2), In newer versions (at least NeoForge’s 1.21.10-based sources), What those overrides are intended to mean (per the class shape shown in mappings/docs) is “value equality” based on its stored holders—i.e., the Sources: 🌐 Web query:
💡 Result: In Minecraft 1.20.x, So:
If you need content-based equality/stable hashing, you must compute it yourself (e.g., from Citations:
🏁 Script executed: # Check build configuration for Minecraft version
find . -name "gradle.properties" -o -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" | head -5Repository: CodeMonkeysMods/MineTale Length of output: 102 🏁 Script executed: # Search for minecraft version strings in config files
rg -n "minecraft" gradle.properties build.gradle build.gradle.kts 2>/dev/null | head -20Repository: CodeMonkeysMods/MineTale Length of output: 193
The ingredient aggregation logic (stream → collect → HolderSet.direct → HashMap lookup per ingredient) runs three times per render tick at lines 198–200 to update button states. This is unnecessary: cache the results and update only when the player's inventory changes (via a change listener) or when the selected recipe changes, not every frame. While 🤖 Prompt for AI Agents |
||
|
|
||
| private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) { | ||
| System.out.println("DEBUG: Searching inventory + nearby for " + totalRequired + "..."); | ||
| if (totalRequired <= 0) return true; | ||
|
|
||
| int found = 0; | ||
|
|
||
| // 1. Check Player Inventory | ||
| for (int i = 0; i < inventory.getContainerSize(); i++) { | ||
| ItemStack stack = inventory.getItem(i); | ||
| if (!stack.isEmpty() && ingredient.test(stack)) { | ||
| found += stack.getCount(); | ||
| } | ||
| } | ||
|
|
||
| // 2. CHECK THE NETWORKED ITEMS FROM CHESTS | ||
| // This is the list we sent via the packet! | ||
| if (this.menu instanceof AbstractWorkbenchContainerMenu workbenchMenu) { | ||
| for (ItemStack stack : workbenchMenu.getNetworkedNearbyItems()) { | ||
| if (!stack.isEmpty() && ingredient.test(stack)) { | ||
| found += stack.getCount(); | ||
| System.out.println("DEBUG: Found " + stack.getCount() + " in nearby networked list. Total: " + found); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (found >= totalRequired) { | ||
| System.out.println("DEBUG: Requirement MET with " + found + "/" + totalRequired); | ||
| return true; | ||
| } | ||
|
|
||
| System.out.println("DEBUG: FAILED. Only found: " + found + "/" + totalRequired); | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Computes the on-screen position for the recipe book toggle button for this GUI. | ||
| * | ||
| * @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center | ||
| */ | ||
| @Override | ||
| protected ScreenPosition getRecipeBookButtonPosition() { | ||
| // 1. Calculate the start (left) of your workbench GUI | ||
| int guiLeft = (this.width - this.imageWidth) / 2; | ||
|
|
||
| // 2. Calculate the top of your workbench GUI | ||
| int guiTop = (this.height - this.imageHeight) / 2; | ||
|
|
||
| // 3. Standard Vanilla positioning: | ||
| // Usually 5 pixels in from the left and 49 pixels up from the center | ||
| return new ScreenPosition(guiLeft + 5, guiTop + this.imageHeight / 2 - 49); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null-pointer risk:
context.client().playermay benullduring retries.The retry loop re-submits itself across ticks. If the player disconnects mid-retry,
context.client().playerwill benull, causing an NPE at Line 44.Proposed fix
`@Override` public void run() { - if (context.client().player.containerMenu instanceof AbstractWorkbenchContainerMenu menu) { + if (context.client().player != null && context.client().player.containerMenu instanceof AbstractWorkbenchContainerMenu menu) { applyItemsToMenu(menu, items, context.client().screen); } else if (retries < 10) { // Try for up to 10 frames (~0.5 seconds)📝 Committable suggestion
🤖 Prompt for AI Agents