Skip to content

Commit 43cf082

Browse files
Merge pull request #30 from CodeMonkeysMods/feat/add-workbench
Add normal Workbench
2 parents 7e94572 + 185249b commit 43cf082

37 files changed

Lines changed: 2232 additions & 282 deletions
Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,66 @@
11
package com.tcm.MineTale;
22

33
import com.tcm.MineTale.block.workbenches.screen.FurnaceWorkbenchScreen;
4+
import com.tcm.MineTale.block.workbenches.screen.WorkbenchWorkbenchScreen;
5+
import com.tcm.MineTale.network.ClientboundNearbyInventorySyncPacket;
6+
7+
import java.util.List;
8+
9+
import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu;
410
import com.tcm.MineTale.block.workbenches.screen.CampfireWorkbenchScreen;
511
import com.tcm.MineTale.registry.ModMenuTypes;
612

713
import net.fabricmc.api.ClientModInitializer;
14+
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
815
import net.minecraft.client.gui.screens.MenuScreens;
16+
import net.minecraft.client.gui.screens.Screen;
17+
import net.minecraft.client.gui.screens.recipebook.RecipeUpdateListener;
18+
import net.minecraft.world.item.ItemStack;
919

1020
public class MineTaleClient implements ClientModInitializer {
11-
12-
13-
1421
/**
15-
* Registers client-side screen factories for custom workbench menu types.
22+
* Register client-side screen factories for custom workbench menu types.
1623
*
17-
* Binds the furnace and campfire workbench menu types to their corresponding screen constructors
18-
* so the client can create the appropriate GUI when those menus are opened.
24+
* Binds ModMenuTypes.FURNACE_WORKBENCH_MENU to FurnaceWorkbenchScreen,
25+
* ModMenuTypes.CAMPFIRE_WORKBENCH_MENU to CampfireWorkbenchScreen, and
26+
* ModMenuTypes.WORKBENCH_WORKBENCH_MENU to WorkbenchWorkbenchScreen so the client
27+
* can create the appropriate GUI when those menus open.
1928
*/
2029
@Override
2130
public void onInitializeClient() {
2231
MenuScreens.register(ModMenuTypes.FURNACE_WORKBENCH_MENU, FurnaceWorkbenchScreen::new);
2332
MenuScreens.register(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, CampfireWorkbenchScreen::new);
33+
MenuScreens.register(ModMenuTypes.WORKBENCH_WORKBENCH_MENU, WorkbenchWorkbenchScreen::new);
34+
35+
ClientPlayNetworking.registerGlobalReceiver(ClientboundNearbyInventorySyncPacket.TYPE, (payload, context) -> {
36+
List<ItemStack> items = payload.items();
37+
38+
// We create a task that can re-run itself if the menu isn't ready yet
39+
context.client().execute(new Runnable() {
40+
int retries = 0;
41+
42+
@Override
43+
public void run() {
44+
if (context.client().player != null && context.client().player.containerMenu instanceof AbstractWorkbenchContainerMenu menu) {
45+
applyItemsToMenu(menu, items, context.client().screen);
46+
} else if (retries < 10) { // Try for up to 10 frames (~0.5 seconds)
47+
retries++;
48+
// Re-submit to the next tick
49+
context.client().execute(this);
50+
} else {
51+
System.out.println("CLIENT: Failed to sync nearby items after 10 retries.");
52+
}
53+
}
54+
});
55+
});
56+
}
57+
58+
// Helper method to keep things clean
59+
private static void applyItemsToMenu(AbstractWorkbenchContainerMenu menu, List<ItemStack> items, Screen screen) {
60+
System.out.println("CLIENT: Successfully applied " + items.size() + " stacks to the Workbench Menu.");
61+
menu.setNetworkedNearbyItems(items);
62+
if (screen instanceof RecipeUpdateListener listener) {
63+
listener.recipesUpdated();
64+
}
2465
}
2566
}
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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

Comments
 (0)