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 .BuildersWorkbenchMenu ;
11+ import com .tcm .MineTale .mixin .client .ClientRecipeBookAccessor ;
12+ import com .tcm .MineTale .network .CraftRequestPayload ;
13+ import com .tcm .MineTale .recipe .MineTaleRecipeBookComponent ;
14+ import com .tcm .MineTale .registry .ModBlocks ;
15+ import com .tcm .MineTale .registry .ModRecipeDisplay ;
16+ import com .tcm .MineTale .registry .ModRecipes ;
17+
18+ import net .fabricmc .fabric .api .client .networking .v1 .ClientPlayNetworking ;
19+ import net .minecraft .client .ClientRecipeBook ;
20+ import net .minecraft .client .gui .GuiGraphics ;
21+ import net .minecraft .client .gui .components .Button ;
22+ import net .minecraft .client .gui .navigation .ScreenPosition ;
23+ import net .minecraft .client .gui .screens .inventory .AbstractRecipeBookScreen ;
24+ import net .minecraft .client .gui .screens .recipebook .RecipeBookComponent ;
25+ import net .minecraft .client .renderer .RenderPipelines ;
26+ import net .minecraft .core .Holder ;
27+ import net .minecraft .resources .Identifier ;
28+ import net .minecraft .world .entity .player .Inventory ;
29+ import net .minecraft .world .entity .player .Player ;
30+ import net .minecraft .world .item .Item ;
31+ import net .minecraft .world .item .ItemStack ;
32+ import net .minecraft .world .item .crafting .Ingredient ;
33+ import net .minecraft .world .item .crafting .display .RecipeDisplayEntry ;
34+ import net .minecraft .world .item .crafting .display .RecipeDisplayId ;
35+ import net .minecraft .world .item .crafting .display .SlotDisplayContext ;
36+ import net .minecraft .network .chat .Component ;
37+
38+ public class BuildersWorkbenchScreen extends AbstractRecipeBookScreen <BuildersWorkbenchMenu > {
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 BuildersWorkbenchScreen (BuildersWorkbenchMenu menu , Inventory inventory , Component title ) {
58+ this (menu , inventory , title , createRecipeBookComponent (menu ));
59+ }
60+
61+ /**
62+ * Creates a BuildersWorkbenchScreen 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 BuildersWorkbenchScreen (BuildersWorkbenchMenu 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 (BuildersWorkbenchMenu menu ) {
81+ ItemStack tabIcon = new ItemStack (ModBlocks .BUILDERS_WORKBENCH_BLOCK .asItem ());
82+
83+ List <RecipeBookComponent .TabInfo > tabs = List .of (
84+ new RecipeBookComponent .TabInfo (tabIcon .getItem (), ModRecipeDisplay .BUILDERS_SEARCH )
85+ );
86+
87+ return new MineTaleRecipeBookComponent (menu , tabs , ModRecipes .BUILDERS_TYPE );
88+ }
89+
90+ /**
91+ * Configure the screen's GUI dimensions and initialize widgets.
92+ *
93+ * Sets the layout size (imageWidth = 176, imageHeight = 166), delegates remaining
94+ * layout initialization to the superclass, and creates the three craft buttons
95+ * ("1", "10", "All") wired to their respective handlers.
96+ */
97+ @ Override
98+ protected void init () {
99+ // Important: Set your GUI size before super.init()
100+ this .imageWidth = 176 ;
101+ this .imageHeight = 166 ;
102+
103+ super .init ();
104+
105+ int defaultLeft = this .leftPos + 90 ;
106+ int defaultTop = this .topPos + 25 ;
107+
108+ this .craftOneBtn = addRenderableWidget (Button .builder (Component .literal ("Craft" ), (button ) -> {
109+ handleCraftRequest (1 );
110+ }).bounds (defaultLeft , defaultTop , 75 , 20 ).build ());
111+
112+ this .craftTenBtn = addRenderableWidget (Button .builder (Component .literal ("x10" ), (button ) -> {
113+ handleCraftRequest (10 );
114+ }).bounds (defaultLeft , defaultTop + 22 , 35 , 20 ).build ());
115+
116+ this .craftAllBtn = addRenderableWidget (Button .builder (Component .literal ("All" ), (button ) -> {
117+ handleCraftRequest (-1 ); // -1 represents "All" logic
118+ }).bounds (defaultLeft + 40 , defaultTop + 22 , 35 , 20 ).build ());
119+ }
120+
121+ /**
122+ * Sends a crafting request for the currently selected recipe in the integrated recipe book.
123+ *
124+ * Locates the last recipe collection and last selected recipe ID from the recipe book component,
125+ * resolves the recipe's result item, and sends a CraftRequestPayload to the server containing that
126+ * item and the requested amount.
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 GUI background texture at the screen's top-left corner.
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 relative to the window
154+ * @param j current mouse y coordinate relative to the window
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+ @ Override
163+ public void render (GuiGraphics graphics , int mouseX , int mouseY , float delta ) {
164+ renderBackground (graphics , mouseX , mouseY , delta );
165+ super .render (graphics , mouseX , mouseY , delta );
166+
167+ // 1. Get the current selection from the book
168+ RecipeDisplayId currentId = this .mineTaleRecipeBook .getSelectedRecipeId ();
169+
170+ // 2. If it's NOT null, remember it!
171+ if (currentId != null ) {
172+ this .lastKnownSelectedId = currentId ;
173+ }
174+
175+ // 3. Use the remembered ID to find the entry for button activation
176+ RecipeDisplayEntry selectedEntry = null ;
177+ if (this .lastKnownSelectedId != null && this .minecraft .level != null ) {
178+ ClientRecipeBook book = this .minecraft .player .getRecipeBook ();
179+ selectedEntry = ((ClientRecipeBookAccessor ) book ).getKnown ().get (this .lastKnownSelectedId );
180+ }
181+
182+ // 2. Button Activation Logic
183+ if (selectedEntry != null ) {
184+ // We use the entry directly. It contains the 15 ingredients needed!
185+ boolean canCraftOne = canCraft (this .minecraft .player , selectedEntry , 1 );
186+ boolean canCraftMoreThanOne = canCraft (this .minecraft .player , selectedEntry , 2 );
187+ boolean canCraftTen = canCraft (this .minecraft .player , selectedEntry , 10 );
188+
189+ this .craftOneBtn .active = canCraftOne ;
190+ this .craftTenBtn .active = canCraftTen ;
191+ this .craftAllBtn .active = canCraftMoreThanOne ;
192+ } else {
193+ this .craftOneBtn .active = false ;
194+ this .craftTenBtn .active = false ;
195+ this .craftAllBtn .active = false ;
196+ }
197+
198+ renderTooltip (graphics , mouseX , mouseY );
199+ }
200+
201+ /**
202+ * Determines whether the player has enough ingredients to craft the given recipe the specified number of times.
203+ *
204+ * @param player the player whose inventory (and networked nearby items) will be checked; may be null
205+ * @param entry the recipe display entry providing crafting requirements; may be null
206+ * @param craftCount the multiplier for required ingredient quantities (e.g., 1, 10, or -1 is not specially handled here)
207+ * @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)
208+ */
209+ private boolean canCraft (Player player , RecipeDisplayEntry entry , int craftCount ) {
210+ if (player == null || entry == null ) return false ;
211+
212+ Optional <List <Ingredient >> reqs = entry .craftingRequirements ();
213+ if (reqs .isEmpty ()) return false ;
214+
215+ // 1. Group ingredients by their underlying Item Holders.
216+ // Using List<Holder<Item>> as the key ensures structural equality (content-based hashing).
217+ Map <List <Holder <Item >>, Integer > aggregatedRequirements = new HashMap <>();
218+ Map <List <Holder <Item >>, Ingredient > holderToIngredient = new HashMap <>();
219+
220+ for (Ingredient ing : reqs .get ()) {
221+ // Collect holders into a List to get a stable hashCode() and equals()
222+ @ SuppressWarnings ("deprecation" )
223+ List <Holder <Item >> key = ing .items ().toList ();
224+
225+ // Aggregate the counts (how many of this specific ingredient set are required)
226+ aggregatedRequirements .put (key , aggregatedRequirements .getOrDefault (key , 0 ) + 1 );
227+
228+ // Map the list back to the original ingredient for use in hasIngredientAmount
229+ holderToIngredient .putIfAbsent (key , ing );
230+ }
231+
232+ // 2. Check the player's inventory against the aggregated totals
233+ Inventory inv = player .getInventory ();
234+ for (Map .Entry <List <Holder <Item >>, Integer > entryReq : aggregatedRequirements .entrySet ()) {
235+ List <Holder <Item >> key = entryReq .getKey ();
236+ int totalNeeded = entryReq .getValue () * craftCount ;
237+
238+ // Retrieve the original Ingredient object associated with this list of holders
239+ Ingredient originalIng = holderToIngredient .get (key );
240+
241+ if (!hasIngredientAmount (inv , originalIng , totalNeeded )) {
242+ return false ;
243+ }
244+ }
245+
246+ return true ;
247+ }
248+
249+ private boolean hasIngredientAmount (Inventory inventory , Ingredient ingredient , int totalRequired ) {
250+ System .out .println ("DEBUG: Searching inventory + nearby for " + totalRequired + "..." );
251+ if (totalRequired <= 0 ) return true ;
252+
253+ int found = 0 ;
254+
255+ // 1. Check Player Inventory
256+ for (int i = 0 ; i < inventory .getContainerSize (); i ++) {
257+ ItemStack stack = inventory .getItem (i );
258+ if (!stack .isEmpty () && ingredient .test (stack )) {
259+ found += stack .getCount ();
260+ }
261+ }
262+
263+ // 2. CHECK THE NETWORKED ITEMS FROM CHESTS
264+ // This is the list we sent via the packet!
265+ if (this .menu instanceof AbstractWorkbenchContainerMenu workbenchMenu ) {
266+ for (ItemStack stack : workbenchMenu .getNetworkedNearbyItems ()) {
267+ if (!stack .isEmpty () && ingredient .test (stack )) {
268+ found += stack .getCount ();
269+ System .out .println ("DEBUG: Found " + stack .getCount () + " in nearby networked list. Total: " + found );
270+ }
271+ }
272+ }
273+
274+ if (found >= totalRequired ) {
275+ System .out .println ("DEBUG: Requirement MET with " + found + "/" + totalRequired );
276+ return true ;
277+ }
278+
279+ System .out .println ("DEBUG: FAILED. Only found: " + found + "/" + totalRequired );
280+ return false ;
281+ }
282+
283+ /**
284+ * Computes the on-screen position for the recipe book toggle button for this GUI.
285+ *
286+ * @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center
287+ */
288+ @ Override
289+ protected ScreenPosition getRecipeBookButtonPosition () {
290+ // 1. Calculate the start (left) of your workbench GUI
291+ int guiLeft = (this .width - this .imageWidth ) / 2 ;
292+
293+ // 2. Calculate the top of your workbench GUI
294+ int guiTop = (this .height - this .imageHeight ) / 2 ;
295+
296+ // 3. Standard Vanilla positioning:
297+ // Usually 5 pixels in from the left and 49 pixels up from the center
298+ return new ScreenPosition (guiLeft + 5 , guiTop + this .imageHeight / 2 - 49 );
299+ }
300+ }
0 commit comments