1+ package com .tcm .MineTale .block .workbenches .screen ;
2+
3+ import com .tcm .MineTale .MineTale ;
4+ import com .tcm .MineTale .block .workbenches .menu .AbstractWorkbenchContainerMenu ;
5+ import com .tcm .MineTale .block .workbenches .menu .AlchemistsWorkbenchMenu ;
6+ import com .tcm .MineTale .mixin .client .ClientRecipeBookAccessor ;
7+ import com .tcm .MineTale .network .CraftRequestPayload ;
8+ import com .tcm .MineTale .recipe .MineTaleRecipeBookComponent ;
9+ import com .tcm .MineTale .registry .ModBlocks ;
10+ import com .tcm .MineTale .registry .ModRecipeDisplay ;
11+ import com .tcm .MineTale .registry .ModRecipes ;
12+ import net .fabricmc .fabric .api .client .networking .v1 .ClientPlayNetworking ;
13+ import net .minecraft .client .ClientRecipeBook ;
14+ import net .minecraft .client .gui .GuiGraphics ;
15+ import net .minecraft .client .gui .components .Button ;
16+ import net .minecraft .client .gui .navigation .ScreenPosition ;
17+ import net .minecraft .client .gui .screens .inventory .AbstractRecipeBookScreen ;
18+ import net .minecraft .client .gui .screens .recipebook .RecipeBookComponent ;
19+ import net .minecraft .client .renderer .RenderPipelines ;
20+ import net .minecraft .core .Holder ;
21+ import net .minecraft .network .chat .Component ;
22+ import net .minecraft .resources .Identifier ;
23+ import net .minecraft .world .entity .player .Inventory ;
24+ import net .minecraft .world .entity .player .Player ;
25+ import net .minecraft .world .item .Item ;
26+ import net .minecraft .world .item .ItemStack ;
27+ import net .minecraft .world .item .crafting .Ingredient ;
28+ import net .minecraft .world .item .crafting .display .RecipeDisplayEntry ;
29+ import net .minecraft .world .item .crafting .display .RecipeDisplayId ;
30+ import net .minecraft .world .item .crafting .display .SlotDisplayContext ;
31+
32+ import java .util .HashMap ;
33+ import java .util .List ;
34+ import java .util .Map ;
35+ import java .util .Optional ;
36+
37+ public class AlchemistsWorkbenchScreen extends AbstractRecipeBookScreen <AlchemistsWorkbenchMenu > {
38+ private static final Identifier TEXTURE =
39+ Identifier .fromNamespaceAndPath (MineTale .MOD_ID , "textures/gui/container/workbench_workbench.png" );
40+
41+ private final MineTaleRecipeBookComponent mineTaleRecipeBook ;
42+
43+ private RecipeDisplayId lastKnownSelectedId = null ;
44+
45+ private Button craftOneBtn ;
46+ private Button craftTenBtn ;
47+ private Button craftAllBtn ;
48+
49+ /**
50+ * Initialize a workbench GUI screen using the provided container menu, player inventory, and title.
51+ *
52+ * @param menu the menu supplying slots and synchronized state for this screen
53+ * @param inventory the player's inventory to display and interact with
54+ * @param title the title component shown at the top of the screen
55+ */
56+ public AlchemistsWorkbenchScreen (AlchemistsWorkbenchMenu menu , Inventory inventory , Component title ) {
57+ this (menu , inventory , title , createRecipeBookComponent (menu ));
58+ }
59+
60+ /**
61+ * Creates a WorkbenchWorkbenchScreen bound to the given menu, player inventory, title, and recipe book component.
62+ *
63+ * @param menu the menu backing this screen
64+ * @param inventory the player's inventory shown in the screen
65+ * @param title the screen title component
66+ * @param recipeBook the MineTaleRecipeBookComponent used to display and manage recipes in this screen
67+ */
68+ private AlchemistsWorkbenchScreen (AlchemistsWorkbenchMenu menu , Inventory inventory , Component title , MineTaleRecipeBookComponent recipeBook ) {
69+ super (menu , recipeBook , inventory , title );
70+ this .mineTaleRecipeBook = recipeBook ;
71+ }
72+
73+ /**
74+ * Create a MineTaleRecipeBookComponent configured for the workbench screen.
75+ *
76+ * @param menu the workbench menu used to initialize the recipe book component
77+ * @return a MineTaleRecipeBookComponent containing the workbench tab and associated recipe category
78+ */
79+ private static MineTaleRecipeBookComponent createRecipeBookComponent (AlchemistsWorkbenchMenu menu ) {
80+ ItemStack tabIcon = new ItemStack (ModBlocks .ALCHEMISTS_WORKBENCH_BLOCK .asItem ());
81+
82+ List <RecipeBookComponent .TabInfo > tabs = List .of (
83+ new RecipeBookComponent .TabInfo (tabIcon .getItem (), ModRecipeDisplay .ALCHEMISTS_SEARCH )
84+ );
85+
86+ return new MineTaleRecipeBookComponent (menu , tabs , ModRecipes .ALCHEMISTS_TYPE );
87+ }
88+
89+ /**
90+ * Initialises the workbench screen's GUI size and interactive widgets.
91+ *
92+ * Sets the screen image dimensions, delegates remaining setup to the superclass,
93+ * computes default button positions and creates three craft buttons:
94+ * - "Craft" (requests 1),
95+ * - "x10" (requests 10),
96+ * - "All" (requests -1 to indicate all).
97+ */
98+ @ Override
99+ protected void init () {
100+ // Important: Set your GUI size before super.init()
101+ this .imageWidth = 176 ;
102+ this .imageHeight = 166 ;
103+
104+ super .init ();
105+
106+ int defaultLeft = this .leftPos + 90 ;
107+ int defaultTop = this .topPos + 25 ;
108+
109+ this .craftOneBtn = addRenderableWidget (Button .builder (Component .translatable ("gui.minetale.craftbtn" ), (button ) -> {
110+ handleCraftRequest (1 );
111+ }).bounds (defaultLeft , defaultTop , 75 , 20 ).build ());
112+
113+ this .craftTenBtn = addRenderableWidget (Button .builder (Component .literal ("x10" ), (button ) -> {
114+ handleCraftRequest (10 );
115+ }).bounds (defaultLeft , defaultTop + 22 , 35 , 20 ).build ());
116+
117+ this .craftAllBtn = addRenderableWidget (Button .builder (Component .translatable ("gui.minetale.allbtn" ), (button ) -> {
118+ handleCraftRequest (-1 ); // -1 represents "All" logic
119+ }).bounds (defaultLeft + 40 , defaultTop + 22 , 35 , 20 ).build ());
120+ }
121+
122+ /**
123+ * Request crafting for the currently selected recipe from the integrated recipe book.
124+ *
125+ * If a recipe is selected, sends a CraftRequestPayload to the server for that recipe and the
126+ * specified quantity. If no recipe is selected, no request is sent.
127+ *
128+ * @param amount the quantity to craft; use -1 to request crafting of the full available stack ("All")
129+ */
130+ private void handleCraftRequest (int amount ) {
131+ // Look at our "Memory" instead of the component
132+ if (this .lastKnownSelectedId != null ) {
133+ ClientRecipeBook book = this .minecraft .player .getRecipeBook ();
134+ RecipeDisplayEntry entry = ((ClientRecipeBookAccessor ) book ).getKnown ().get (this .lastKnownSelectedId );
135+
136+ if (entry != null ) {
137+ List <ItemStack > results = entry .resultItems (SlotDisplayContext .fromLevel (this .minecraft .level ));
138+ if (!results .isEmpty ()) {
139+ System .out .println ("Persistent Selection Success: " + results .get (0 ));
140+ ClientPlayNetworking .send (new CraftRequestPayload (results .get (0 ), amount ));
141+ return ;
142+ }
143+ }
144+ }
145+ System .out .println ("Request failed: No recipe was ever selected!" );
146+ }
147+
148+ /**
149+ * Draws the workbench background texture at the screen's current GUI origin.
150+ *
151+ * @param guiGraphics the graphics context used to draw GUI elements
152+ * @param f partial tick time for interpolation
153+ * @param i current mouse x coordinate
154+ * @param j current mouse y coordinate
155+ */
156+ protected void renderBg (GuiGraphics guiGraphics , float f , int i , int j ) {
157+ int k = this .leftPos ;
158+ int l = this .topPos ;
159+ guiGraphics .blit (RenderPipelines .GUI_TEXTURED , TEXTURE , k , l , 0.0F , 0.0F , this .imageWidth , this .imageHeight , 256 , 256 );
160+ }
161+
162+ /**
163+ * Render the screen, remember the current recipe selection and update craft-button availability.
164+ *
165+ * Remembers the recipe selected in the recipe book, resolves that selection against the client's known recipes when possible,
166+ * sets the craft buttons active or inactive according to whether the player has sufficient ingredients for counts of 1, 2 and 10,
167+ * renders the background, the superclass UI and any tooltips.
168+ */
169+ @ Override
170+ public void render (GuiGraphics graphics , int mouseX , int mouseY , float delta ) {
171+ renderBackground (graphics , mouseX , mouseY , delta );
172+ super .render (graphics , mouseX , mouseY , delta );
173+
174+ // 1. Get the current selection from the book
175+ RecipeDisplayId currentId = this .mineTaleRecipeBook .getSelectedRecipeId ();
176+
177+ // 2. If it's NOT null, remember it!
178+ if (currentId != null ) {
179+ this .lastKnownSelectedId = currentId ;
180+ }
181+
182+ // 3. Use the remembered ID to find the entry for button activation
183+ RecipeDisplayEntry selectedEntry = null ;
184+ if (this .lastKnownSelectedId != null && this .minecraft .level != null ) {
185+ ClientRecipeBook book = this .minecraft .player .getRecipeBook ();
186+ selectedEntry = ((ClientRecipeBookAccessor ) book ).getKnown ().get (this .lastKnownSelectedId );
187+ }
188+
189+ // 2. Button Activation Logic
190+ if (selectedEntry != null ) {
191+ // We use the entry directly. It contains the 15 ingredients needed!
192+ boolean canCraftOne = canCraft (this .minecraft .player , selectedEntry , 1 );
193+ boolean canCraftMoreThanOne = canCraft (this .minecraft .player , selectedEntry , 2 );
194+ boolean canCraftTen = canCraft (this .minecraft .player , selectedEntry , 10 );
195+
196+ this .craftOneBtn .active = canCraftOne ;
197+ this .craftTenBtn .active = canCraftTen ;
198+ this .craftAllBtn .active = canCraftMoreThanOne ;
199+ } else {
200+ this .craftOneBtn .active = false ;
201+ this .craftTenBtn .active = false ;
202+ this .craftAllBtn .active = false ;
203+ }
204+
205+ renderTooltip (graphics , mouseX , mouseY );
206+ }
207+
208+ /**
209+ * Determines whether the player has enough ingredients to craft the given recipe the specified number of times.
210+ *
211+ * @param player the player whose inventory (and networked nearby items) will be checked; may be null
212+ * @param entry the recipe display entry providing crafting requirements; may be null
213+ * @param craftCount the multiplier for required ingredient quantities (e.g., 1, 10, or -1 is not specially handled here)
214+ * @return `true` if the player has at least the required quantity of each ingredient multiplied by `craftCount`, `false` otherwise (also returns `false` if `player` or `entry` is null or the recipe has no requirements)
215+ */
216+ private boolean canCraft (Player player , RecipeDisplayEntry entry , int craftCount ) {
217+ if (player == null || entry == null ) return false ;
218+
219+ Optional <List <Ingredient >> reqs = entry .craftingRequirements ();
220+ if (reqs .isEmpty ()) return false ;
221+
222+ // 1. Group ingredients by their underlying Item Holders.
223+ // Using List<Holder<Item>> as the key ensures structural equality (content-based hashing).
224+ Map <List <Holder <Item >>, Integer > aggregatedRequirements = new HashMap <>();
225+ Map <List <Holder <Item >>, Ingredient > holderToIngredient = new HashMap <>();
226+
227+ for (Ingredient ing : reqs .get ()) {
228+ // Collect holders into a List to get a stable hashCode() and equals()
229+ @ SuppressWarnings ("deprecation" )
230+ List <Holder <Item >> key = ing .items ().toList ();
231+
232+ // Aggregate the counts (how many of this specific ingredient set are required)
233+ aggregatedRequirements .put (key , aggregatedRequirements .getOrDefault (key , 0 ) + 1 );
234+
235+ // Map the list back to the original ingredient for use in hasIngredientAmount
236+ holderToIngredient .putIfAbsent (key , ing );
237+ }
238+
239+ // 2. Check the player's inventory against the aggregated totals
240+ Inventory inv = player .getInventory ();
241+ for (Map .Entry <List <Holder <Item >>, Integer > entryReq : aggregatedRequirements .entrySet ()) {
242+ List <Holder <Item >> key = entryReq .getKey ();
243+ int totalNeeded = entryReq .getValue () * craftCount ;
244+
245+ // Retrieve the original Ingredient object associated with this list of holders
246+ Ingredient originalIng = holderToIngredient .get (key );
247+
248+ if (!hasIngredientAmount (inv , originalIng , totalNeeded )) {
249+ return false ;
250+ }
251+ }
252+
253+ return true ;
254+ }
255+
256+ private boolean hasIngredientAmount (Inventory inventory , Ingredient ingredient , int totalRequired ) {
257+ System .out .println ("DEBUG: Searching inventory + nearby for " + totalRequired + "..." );
258+ if (totalRequired <= 0 ) return true ;
259+
260+ int found = 0 ;
261+
262+ // 1. Check Player Inventory
263+ for (int i = 0 ; i < inventory .getContainerSize (); i ++) {
264+ ItemStack stack = inventory .getItem (i );
265+ if (!stack .isEmpty () && ingredient .test (stack )) {
266+ found += stack .getCount ();
267+ }
268+ }
269+
270+ // 2. CHECK THE NETWORKED ITEMS FROM CHESTS
271+ // This is the list we sent via the packet!
272+ if (this .menu instanceof AbstractWorkbenchContainerMenu workbenchMenu ) {
273+ for (ItemStack stack : workbenchMenu .getNetworkedNearbyItems ()) {
274+ if (!stack .isEmpty () && ingredient .test (stack )) {
275+ found += stack .getCount ();
276+ System .out .println ("DEBUG: Found " + stack .getCount () + " in nearby networked list. Total: " + found );
277+ }
278+ }
279+ }
280+
281+ if (found >= totalRequired ) {
282+ System .out .println ("DEBUG: Requirement MET with " + found + "/" + totalRequired );
283+ return true ;
284+ }
285+
286+ System .out .println ("DEBUG: FAILED. Only found: " + found + "/" + totalRequired );
287+ return false ;
288+ }
289+
290+ /**
291+ * Computes the on-screen position for the recipe book toggle button for this GUI.
292+ *
293+ * @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center
294+ */
295+ @ Override
296+ protected ScreenPosition getRecipeBookButtonPosition () {
297+ // 1. Calculate the start (left) of your workbench GUI
298+ int guiLeft = (this .width - this .imageWidth ) / 2 ;
299+
300+ // 2. Calculate the top of your workbench GUI
301+ int guiTop = (this .height - this .imageHeight ) / 2 ;
302+
303+ // 3. Standard Vanilla positioning:
304+ // Usually 5 pixels in from the left and 49 pixels up from the center
305+ return new ScreenPosition (guiLeft + 5 , guiTop + this .imageHeight / 2 - 49 );
306+ }
307+ }
0 commit comments