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