Skip to content

Commit f42074a

Browse files
feat: add most recipes from farmers workbench
1 parent 1d06564 commit f42074a

21 files changed

Lines changed: 1288 additions & 623 deletions

src/client/java/com/tcm/MineTale/MineTaleClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
1010
import com.tcm.MineTale.block.workbenches.screen.ArmorersWorkbenchScreen;
1111
import com.tcm.MineTale.block.workbenches.screen.CampfireWorkbenchScreen;
12+
import com.tcm.MineTale.block.workbenches.screen.FarmersWorkbenchScreen;
1213
import com.tcm.MineTale.registry.ModBlocks;
1314
import com.tcm.MineTale.registry.ModMenuTypes;
1415

@@ -36,6 +37,7 @@ public void onInitializeClient() {
3637
MenuScreens.register(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, CampfireWorkbenchScreen::new);
3738
MenuScreens.register(ModMenuTypes.WORKBENCH_WORKBENCH_MENU, WorkbenchWorkbenchScreen::new);
3839
MenuScreens.register(ModMenuTypes.ARMORERS_WORKBENCH_MENU, ArmorersWorkbenchScreen::new);
40+
MenuScreens.register(ModMenuTypes.FARMERS_WORKBENCH_MENU, FarmersWorkbenchScreen::new);
3941

4042
BlockRenderLayerMap.putBlock(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1, ChunkSectionLayer.CUTOUT);
4143
BlockRenderLayerMap.putBlock(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2, ChunkSectionLayer.CUTOUT);

src/client/java/com/tcm/MineTale/block/workbenches/screen/ArmorersWorkbenchScreen.java

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -118,46 +118,6 @@ protected void init() {
118118
}).bounds(defaultLeft + 40, defaultTop + 22, 35, 20).build());
119119
}
120120

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-
131-
// private void handleCraftRequest(int amount) {
132-
// // 1. Cast the book component to the Accessor to get the selected data
133-
// RecipeBookComponentAccessor accessor = (RecipeBookComponentAccessor) this.mineTaleRecipeBook;
134-
135-
// RecipeCollection collection = accessor.getLastRecipeCollection();
136-
// RecipeDisplayId displayId = accessor.getLastRecipe();
137-
138-
// if (collection != null && displayId != null) {
139-
// // 2. Find the visual entry
140-
// for (RecipeDisplayEntry entry : collection.getSelectedRecipes(RecipeCollection.CraftableStatus.ANY)) {
141-
// if (entry.id().equals(displayId)) {
142-
// // 3. Resolve result for the packet
143-
// List<ItemStack> results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level));
144-
145-
// if (!results.isEmpty()) {
146-
// ItemStack resultStack = results.get(0);
147-
148-
// // 4. LOG FOR DEBUGGING
149-
// System.out.println("Sending craft request for: " + resultStack + " amount: " + amount);
150-
151-
// ClientPlayNetworking.send(new CraftRequestPayload(resultStack, amount));
152-
// }
153-
// break;
154-
// }
155-
// }
156-
// } else {
157-
// System.out.println("Request failed: Collection or DisplayID is null!");
158-
// }
159-
// }
160-
161121
private void handleCraftRequest(int amount) {
162122
// Look at our "Memory" instead of the component
163123
if (this.lastKnownSelectedId != null) {
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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

Comments
 (0)