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