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