diff --git a/src/client/java/com/tcm/MineTale/block/workbenches/screen/ArmorersWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/ArmorersWorkbenchScreen.java index d90484e..af74be8 100644 --- a/src/client/java/com/tcm/MineTale/block/workbenches/screen/ArmorersWorkbenchScreen.java +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/ArmorersWorkbenchScreen.java @@ -72,10 +72,10 @@ private ArmorersWorkbenchScreen(ArmorersWorkbenchMenu menu, Inventory inventory, } /** - * Create a MineTaleRecipeBookComponent configured for the workbench screen. + * Creates a MineTaleRecipeBookComponent preconfigured for the armourer's workbench. * - * @param menu the workbench menu used to initialize the recipe book component - * @return a MineTaleRecipeBookComponent containing the workbench tab and associated recipe category + * @param menu the workbench menu used to bind the recipe book to the current container + * @return the configured MineTaleRecipeBookComponent containing the workbench tab and associated recipe type */ private static MineTaleRecipeBookComponent createRecipeBookComponent(ArmorersWorkbenchMenu menu) { ItemStack tabIcon = new ItemStack(ModBlocks.ARMORERS_WORKBENCH_BLOCK.asItem()); @@ -88,12 +88,12 @@ private static MineTaleRecipeBookComponent createRecipeBookComponent(ArmorersWor } /** - * Configure the screen's GUI dimensions and initialize widgets. - * - * Sets the layout size (imageWidth = 176, imageHeight = 166), delegates remaining - * layout initialization to the superclass, and creates the three craft buttons - * ("1", "10", "All") wired to their respective handlers. - */ + * Initialises the screen layout and registers three crafting buttons. + * + * Sets the GUI image dimensions, delegates further initialisation to the superclass, + * and creates three buttons that request crafting of 1, 10 or all results (the + * "all" action is represented by -1). + */ @Override protected void init() { // Important: Set your GUI size before super.init() @@ -119,45 +119,14 @@ protected void init() { } /** - * Sends a crafting request for the currently selected recipe in the integrated recipe book. + * Send a craft request for the recipe remembered by the screen's last known selection. * - * Locates the last recipe collection and last selected recipe ID from the recipe book component, - * resolves the recipe's result item, and sends a CraftRequestPayload to the server containing that - * item and the requested amount. + * Resolves the remembered recipe to its resulting item(s) and, if a result exists, sends a + * network CraftRequestPayload containing the first result and the specified amount. If there + * is no remembered selection or no results, no network payload is sent. * - * @param amount the quantity to craft; use -1 to request crafting of the full available stack ("All") + * @param amount the quantity to craft; use -1 to request crafting all available units */ - - // private void handleCraftRequest(int amount) { - // // 1. Cast the book component to the Accessor to get the selected data - // RecipeBookComponentAccessor accessor = (RecipeBookComponentAccessor) this.mineTaleRecipeBook; - - // RecipeCollection collection = accessor.getLastRecipeCollection(); - // RecipeDisplayId displayId = accessor.getLastRecipe(); - - // if (collection != null && displayId != null) { - // // 2. Find the visual entry - // for (RecipeDisplayEntry entry : collection.getSelectedRecipes(RecipeCollection.CraftableStatus.ANY)) { - // if (entry.id().equals(displayId)) { - // // 3. Resolve result for the packet - // List results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); - - // if (!results.isEmpty()) { - // ItemStack resultStack = results.get(0); - - // // 4. LOG FOR DEBUGGING - // System.out.println("Sending craft request for: " + resultStack + " amount: " + amount); - - // ClientPlayNetworking.send(new CraftRequestPayload(resultStack, amount)); - // } - // break; - // } - // } - // } else { - // System.out.println("Request failed: Collection or DisplayID is null!"); - // } - // } - private void handleCraftRequest(int amount) { // Look at our "Memory" instead of the component if (this.lastKnownSelectedId != null) { diff --git a/src/client/java/com/tcm/MineTale/block/workbenches/screen/BuildersWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/BuildersWorkbenchScreen.java new file mode 100644 index 0000000..32c4a37 --- /dev/null +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/BuildersWorkbenchScreen.java @@ -0,0 +1,318 @@ +package com.tcm.MineTale.block.workbenches.screen; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.tcm.MineTale.MineTale; +import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; +import com.tcm.MineTale.block.workbenches.menu.BuildersWorkbenchMenu; +import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor; +import com.tcm.MineTale.network.CraftRequestPayload; +import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; +import com.tcm.MineTale.registry.ModBlocks; +import com.tcm.MineTale.registry.ModRecipeDisplay; +import com.tcm.MineTale.registry.ModRecipes; + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.ClientRecipeBook; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.navigation.ScreenPosition; +import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.Holder; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import net.minecraft.network.chat.Component; + +public class BuildersWorkbenchScreen extends AbstractRecipeBookScreen { + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png"); + + private final MineTaleRecipeBookComponent mineTaleRecipeBook; + + private RecipeDisplayId lastKnownSelectedId = null; + + private Button craftOneBtn; + private Button craftTenBtn; + private Button craftAllBtn; + + /** + * Initialize a workbench GUI screen using the provided container menu, player inventory, and title. + * + * @param menu the menu supplying slots and synchronized state for this screen + * @param inventory the player's inventory to display and interact with + * @param title the title component shown at the top of the screen + */ + public BuildersWorkbenchScreen(BuildersWorkbenchMenu menu, Inventory inventory, Component title) { + this(menu, inventory, title, createRecipeBookComponent(menu)); + } + + /** + * Creates a BuildersWorkbenchScreen bound to the given menu, player inventory, title, and recipe book component. + * + * @param menu the menu backing this screen + * @param inventory the player's inventory shown in the screen + * @param title the screen title component + * @param recipeBook the MineTaleRecipeBookComponent used to display and manage recipes in this screen + */ + private BuildersWorkbenchScreen(BuildersWorkbenchMenu menu, Inventory inventory, Component title, MineTaleRecipeBookComponent recipeBook) { + super(menu, recipeBook, inventory, title); + this.mineTaleRecipeBook = recipeBook; + } + + /** + * Create a MineTaleRecipeBookComponent configured for the workbench screen. + * + * @param menu the workbench menu used to initialize the recipe book component + * @return a MineTaleRecipeBookComponent containing the workbench tab and associated recipe category + */ + private static MineTaleRecipeBookComponent createRecipeBookComponent(BuildersWorkbenchMenu menu) { + ItemStack tabIcon = new ItemStack(ModBlocks.BUILDERS_WORKBENCH_BLOCK.asItem()); + + List tabs = List.of( + new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.BUILDERS_SEARCH) + ); + + return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.BUILDERS_TYPE); + } + + /** + * Initialise the screen: set GUI dimensions and create craft buttons. + * + * Sets the screen image width and height, calls superclass initialisation, and adds three + * buttons bound to craft request handlers for quantities 1, 10 and all available items. + */ + @Override + protected void init() { + // Important: Set your GUI size before super.init() + this.imageWidth = 176; + this.imageHeight = 166; + + super.init(); + + int defaultLeft = this.leftPos + 90; + int defaultTop = this.topPos + 25; + + this.craftOneBtn = addRenderableWidget(Button.builder(Component.literal("Craft"), (button) -> { + handleCraftRequest(1); + }).bounds(defaultLeft, defaultTop, 75, 20).build()); + + this.craftTenBtn = addRenderableWidget(Button.builder(Component.literal("x10"), (button) -> { + handleCraftRequest(10); + }).bounds(defaultLeft, defaultTop + 22, 35, 20).build()); + + this.craftAllBtn = addRenderableWidget(Button.builder(Component.literal("All"), (button) -> { + handleCraftRequest(-1); // -1 represents "All" logic + }).bounds(defaultLeft + 40, defaultTop + 22, 35, 20).build()); + } + + /** + * Request crafting for the currently selected recipe in the recipe book. + * + * If a recipe is selected, sends a CraftRequestPayload to the server containing the recipe's primary + * result item and the requested amount. If no recipe is selected, no network request is sent. + * + * @param amount the quantity to craft; use -1 to request crafting of all available items + */ + private void handleCraftRequest(int amount) { + // Look at our "Memory" instead of the component + if (this.lastKnownSelectedId != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + RecipeDisplayEntry entry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + + if (entry != null) { + List results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); + if (!results.isEmpty()) { + System.out.println("Persistent Selection Success: " + results.get(0)); + ClientPlayNetworking.send(new CraftRequestPayload(results.get(0), amount)); + return; + } + } + } + System.out.println("Request failed: No recipe was ever selected!"); + } + + /** + * Render the builders' workbench background texture at the GUI's top-left position. + * + * @param guiGraphics the graphics context used to draw GUI elements + * @param f partial ticks used for frame interpolation + * @param i current mouse x coordinate + * @param j current mouse y coordinate + */ + protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { + int k = this.leftPos; + int l = this.topPos; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256); + } + + /** + * Renders the screen and updates the craft buttons' enabled state based on the current recipe selection. + * + * Uses the recipe currently selected in the bound recipe book (or the last remembered selection) to + * resolve the corresponding RecipeDisplayEntry and enables or disables the "Craft", "x10" and "All" + * buttons according to whether the player has sufficient ingredients for 1, 10 or multiple items. + * + * @param graphics the GuiGraphics context used for drawing + * @param mouseX current mouse X position over the screen + * @param mouseY current mouse Y position over the screen + * @param delta frame delta time in seconds since the last render call + */ + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + renderBackground(graphics, mouseX, mouseY, delta); + super.render(graphics, mouseX, mouseY, delta); + + // 1. Get the current selection from the book + RecipeDisplayId currentId = this.mineTaleRecipeBook.getSelectedRecipeId(); + + // 2. If it's NOT null, remember it! + if (currentId != null) { + this.lastKnownSelectedId = currentId; + } + + // 3. Use the remembered ID to find the entry for button activation + RecipeDisplayEntry selectedEntry = null; + if (this.lastKnownSelectedId != null && this.minecraft.level != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + selectedEntry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + } + + // 2. Button Activation Logic + if (selectedEntry != null) { + // We use the entry directly. It contains the 15 ingredients needed! + boolean canCraftOne = canCraft(this.minecraft.player, selectedEntry, 1); + boolean canCraftMoreThanOne = canCraft(this.minecraft.player, selectedEntry, 2); + boolean canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10); + + this.craftOneBtn.active = canCraftOne; + this.craftTenBtn.active = canCraftTen; + this.craftAllBtn.active = canCraftMoreThanOne; + } else { + this.craftOneBtn.active = false; + this.craftTenBtn.active = false; + this.craftAllBtn.active = false; + } + + renderTooltip(graphics, mouseX, mouseY); + } + + /** + * Determine whether the player has enough ingredients to craft the specified recipe the given number of times. + * + * @param player the player whose inventory and networked nearby items will be checked; may be null + * @param entry the recipe display entry providing crafting requirements; may be null + * @param craftCount the multiplier for required ingredient quantities + * @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) + */ + private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) { + if (player == null || entry == null) return false; + + Optional> reqs = entry.craftingRequirements(); + if (reqs.isEmpty()) return false; + + // 1. Group ingredients by their underlying Item Holders. + // Using List> as the key ensures structural equality (content-based hashing). + Map>, Integer> aggregatedRequirements = new HashMap<>(); + Map>, Ingredient> holderToIngredient = new HashMap<>(); + + for (Ingredient ing : reqs.get()) { + // Collect holders into a List to get a stable hashCode() and equals() + @SuppressWarnings("deprecation") + List> key = ing.items().toList(); + + // Aggregate the counts (how many of this specific ingredient set are required) + aggregatedRequirements.put(key, aggregatedRequirements.getOrDefault(key, 0) + 1); + + // Map the list back to the original ingredient for use in hasIngredientAmount + holderToIngredient.putIfAbsent(key, ing); + } + + // 2. Check the player's inventory against the aggregated totals + Inventory inv = player.getInventory(); + for (Map.Entry>, Integer> entryReq : aggregatedRequirements.entrySet()) { + List> key = entryReq.getKey(); + int totalNeeded = entryReq.getValue() * craftCount; + + // Retrieve the original Ingredient object associated with this list of holders + Ingredient originalIng = holderToIngredient.get(key); + + if (!hasIngredientAmount(inv, originalIng, totalNeeded)) { + return false; + } + } + + return true; + } + + /** + * Determines whether the provided inventory (and any networked nearby items provided by the workbench menu) contains at least the required quantity of items matching the ingredient. + * + * @param inventory the inventory to search + * @param ingredient the ingredient predicate used to match item stacks + * @param totalRequired the total number of matching items required; values less than or equal to zero are treated as satisfied + * @return true if the combined count of matching items found is greater than or equal to totalRequired, false otherwise + */ + private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) { + System.out.println("DEBUG: Searching inventory + nearby for " + totalRequired + "..."); + if (totalRequired <= 0) return true; + + int found = 0; + + // 1. Check Player Inventory + for (int i = 0; i < inventory.getContainerSize(); i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && ingredient.test(stack)) { + found += stack.getCount(); + } + } + + // 2. CHECK THE NETWORKED ITEMS FROM CHESTS + // This is the list we sent via the packet! + if (this.menu instanceof AbstractWorkbenchContainerMenu workbenchMenu) { + for (ItemStack stack : workbenchMenu.getNetworkedNearbyItems()) { + if (!stack.isEmpty() && ingredient.test(stack)) { + found += stack.getCount(); + System.out.println("DEBUG: Found " + stack.getCount() + " in nearby networked list. Total: " + found); + } + } + } + + if (found >= totalRequired) { + System.out.println("DEBUG: Requirement MET with " + found + "/" + totalRequired); + return true; + } + + System.out.println("DEBUG: FAILED. Only found: " + found + "/" + totalRequired); + return false; + } + + /** + * Calculate the screen position for the recipe book toggle button. + * + * @return a ScreenPosition positioned 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical centre + */ + @Override + protected ScreenPosition getRecipeBookButtonPosition() { + // 1. Calculate the start (left) of your workbench GUI + int guiLeft = (this.width - this.imageWidth) / 2; + + // 2. Calculate the top of your workbench GUI + int guiTop = (this.height - this.imageHeight) / 2; + + // 3. Standard Vanilla positioning: + // Usually 5 pixels in from the left and 49 pixels up from the center + return new ScreenPosition(guiLeft + 5, guiTop + this.imageHeight / 2 - 49); + } +} \ No newline at end of file diff --git a/src/client/java/com/tcm/MineTale/block/workbenches/screen/FarmersWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/FarmersWorkbenchScreen.java new file mode 100644 index 0000000..b4c0eb2 --- /dev/null +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/FarmersWorkbenchScreen.java @@ -0,0 +1,315 @@ +package com.tcm.MineTale.block.workbenches.screen; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.tcm.MineTale.MineTale; +import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; +import com.tcm.MineTale.block.workbenches.menu.FarmersWorkbenchMenu; +import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor; +import com.tcm.MineTale.network.CraftRequestPayload; +import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; +import com.tcm.MineTale.registry.ModBlocks; +import com.tcm.MineTale.registry.ModRecipeDisplay; +import com.tcm.MineTale.registry.ModRecipes; + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.ClientRecipeBook; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.navigation.ScreenPosition; +import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.core.Holder; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; +import net.minecraft.world.item.crafting.display.RecipeDisplayId; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import net.minecraft.network.chat.Component; + +public class FarmersWorkbenchScreen extends AbstractRecipeBookScreen { + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png"); + + private final MineTaleRecipeBookComponent mineTaleRecipeBook; + + private RecipeDisplayId lastKnownSelectedId = null; + + private Button craftOneBtn; + private Button craftTenBtn; + private Button craftAllBtn; + + /** + * Creates a FarmersWorkbenchScreen configured with the provided container menu, player inventory, and title. + * + * @param menu the menu supplying slots and synchronized state for this screen + * @param inventory the player's inventory to display and interact with + * @param title the title component shown at the top of the screen + */ + public FarmersWorkbenchScreen(FarmersWorkbenchMenu menu, Inventory inventory, Component title) { + this(menu, inventory, title, createRecipeBookComponent(menu)); + } + + /** + * Creates a FarmersWorkbenchScreen bound to the given menu, player inventory, title, and recipe book component. + * + * @param menu the menu backing this screen + * @param inventory the player's inventory shown in the screen + * @param title the screen title component + * @param recipeBook the MineTaleRecipeBookComponent used to display and manage recipes in this screen + */ + private FarmersWorkbenchScreen(FarmersWorkbenchMenu menu, Inventory inventory, Component title, MineTaleRecipeBookComponent recipeBook) { + super(menu, recipeBook, inventory, title); + this.mineTaleRecipeBook = recipeBook; + } + + /** + * Creates a MineTaleRecipeBookComponent configured for the farmers workbench screen. + * + * @param menu the workbench menu used to initialize the recipe book component + * @return the recipe book component containing the workbench tab and the FARMERS recipe category + */ + private static MineTaleRecipeBookComponent createRecipeBookComponent(FarmersWorkbenchMenu menu) { + ItemStack tabIcon = new ItemStack(ModBlocks.FARMERS_WORKBENCH_BLOCK.asItem()); + + List tabs = List.of( + new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.FARMERS_SEARCH) + ); + + return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.FARMERS_TYPE); + } + + /** + * Initialises the screen layout and registers the crafting controls. + * + * Sets the GUI dimensions to 176×166 and adds three buttons wired to craft requests: + * - "Craft" requests 1, + * - "x10" requests 10, + * - "All" requests all (represented by -1). + */ + @Override + protected void init() { + // Important: Set your GUI size before super.init() + this.imageWidth = 176; + this.imageHeight = 166; + + super.init(); + + int defaultLeft = this.leftPos + 90; + int defaultTop = this.topPos + 25; + + this.craftOneBtn = addRenderableWidget(Button.builder(Component.literal("Craft"), (button) -> { + handleCraftRequest(1); + }).bounds(defaultLeft, defaultTop, 75, 20).build()); + + this.craftTenBtn = addRenderableWidget(Button.builder(Component.literal("x10"), (button) -> { + handleCraftRequest(10); + }).bounds(defaultLeft, defaultTop + 22, 35, 20).build()); + + this.craftAllBtn = addRenderableWidget(Button.builder(Component.literal("All"), (button) -> { + handleCraftRequest(-1); // -1 represents "All" logic + }).bounds(defaultLeft + 40, defaultTop + 22, 35, 20).build()); + } + + /** + * Requests the server to craft the currently selected recipe from the integrated recipe book. + * + * If a previously selected recipe is remembered and has at least one result item, sends a + * CraftRequestPayload containing the recipe's primary result and the requested amount. If no + * remembered selection or no result exists, no request is sent. + * + * @param amount the quantity to craft; use -1 to request crafting the maximum available amount ("All") + */ + private void handleCraftRequest(int amount) { + // Look at our "Memory" instead of the component + if (this.lastKnownSelectedId != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + RecipeDisplayEntry entry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + + if (entry != null) { + List results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); + if (!results.isEmpty()) { + System.out.println("Persistent Selection Success: " + results.get(0)); + ClientPlayNetworking.send(new CraftRequestPayload(results.get(0), amount)); + return; + } + } + } + System.out.println("Request failed: No recipe was ever selected!"); + } + + /** + * Render the workbench GUI background texture at the screen origin. + * + * @param guiGraphics the graphics context used to draw GUI elements + * @param f partial tick delta used for interpolation + * @param i mouse X position relative to the window + * @param j mouse Y position relative to the window + */ + protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { + int k = this.leftPos; + int l = this.topPos; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256); + } + + /** + * Render the screen, persist the last selected recipe and update craft button states. + * + * Remembers the recipe book's current selection so it persists when the book is closed, resolves the corresponding known RecipeDisplayEntry from the player's recipe book when available, enables or disables the craft buttons according to whether the player can craft 1, more than one, or 10 of the selected recipe, and renders the background and tooltips. + */ + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + renderBackground(graphics, mouseX, mouseY, delta); + super.render(graphics, mouseX, mouseY, delta); + + // 1. Get the current selection from the book + RecipeDisplayId currentId = this.mineTaleRecipeBook.getSelectedRecipeId(); + + // 2. If it's NOT null, remember it! + if (currentId != null) { + this.lastKnownSelectedId = currentId; + } + + // 3. Use the remembered ID to find the entry for button activation + RecipeDisplayEntry selectedEntry = null; + if (this.lastKnownSelectedId != null && this.minecraft.level != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + selectedEntry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + } + + // 2. Button Activation Logic + if (selectedEntry != null) { + // We use the entry directly. It contains the 15 ingredients needed! + boolean canCraftOne = canCraft(this.minecraft.player, selectedEntry, 1); + boolean canCraftMoreThanOne = canCraft(this.minecraft.player, selectedEntry, 2); + boolean canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10); + + this.craftOneBtn.active = canCraftOne; + this.craftTenBtn.active = canCraftTen; + this.craftAllBtn.active = canCraftMoreThanOne; + } else { + this.craftOneBtn.active = false; + this.craftTenBtn.active = false; + this.craftAllBtn.active = false; + } + + renderTooltip(graphics, mouseX, mouseY); + } + + /** + * Determine whether the player possesses the ingredients required to craft the given recipe the specified number of times. + * + * @param player the player whose inventory and nearby networked items are considered; may be {@code null} + * @param entry the recipe entry supplying crafting requirements; may be {@code null} + * @param craftCount how many times to craft the recipe (multiplies each ingredient requirement) + * @return {@code true} if the player has at least the required quantity of each ingredient multiplied by {@code craftCount}, {@code false} otherwise + */ + private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) { + if (player == null || entry == null) return false; + + Optional> reqs = entry.craftingRequirements(); + if (reqs.isEmpty()) return false; + + // 1. Group ingredients by their underlying Item Holders. + // Using List> as the key ensures structural equality (content-based hashing). + Map>, Integer> aggregatedRequirements = new HashMap<>(); + Map>, Ingredient> holderToIngredient = new HashMap<>(); + + for (Ingredient ing : reqs.get()) { + // Collect holders into a List to get a stable hashCode() and equals() + @SuppressWarnings("deprecation") + List> key = ing.items().toList(); + + // Aggregate the counts (how many of this specific ingredient set are required) + aggregatedRequirements.put(key, aggregatedRequirements.getOrDefault(key, 0) + 1); + + // Map the list back to the original ingredient for use in hasIngredientAmount + holderToIngredient.putIfAbsent(key, ing); + } + + // 2. Check the player's inventory against the aggregated totals + Inventory inv = player.getInventory(); + for (Map.Entry>, Integer> entryReq : aggregatedRequirements.entrySet()) { + List> key = entryReq.getKey(); + int totalNeeded = entryReq.getValue() * craftCount; + + // Retrieve the original Ingredient object associated with this list of holders + Ingredient originalIng = holderToIngredient.get(key); + + if (!hasIngredientAmount(inv, originalIng, totalNeeded)) { + return false; + } + } + + return true; + } + + /** + * Determine whether the player inventory together with the workbench's networked nearby items + * contains at least the specified quantity of items matching the given ingredient. + * + * @param inventory the player's inventory to check + * @param ingredient the ingredient matcher used to test item stacks + * @param totalRequired the total number of matching items required + * @return true if the combined sources contain at least totalRequired matching items, false otherwise + */ + private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) { + System.out.println("DEBUG: Searching inventory + nearby for " + totalRequired + "..."); + if (totalRequired <= 0) return true; + + int found = 0; + + // 1. Check Player Inventory + for (int i = 0; i < inventory.getContainerSize(); i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && ingredient.test(stack)) { + found += stack.getCount(); + } + } + + // 2. CHECK THE NETWORKED ITEMS FROM CHESTS + // This is the list we sent via the packet! + if (this.menu instanceof AbstractWorkbenchContainerMenu workbenchMenu) { + for (ItemStack stack : workbenchMenu.getNetworkedNearbyItems()) { + if (!stack.isEmpty() && ingredient.test(stack)) { + found += stack.getCount(); + System.out.println("DEBUG: Found " + stack.getCount() + " in nearby networked list. Total: " + found); + } + } + } + + if (found >= totalRequired) { + System.out.println("DEBUG: Requirement MET with " + found + "/" + totalRequired); + return true; + } + + System.out.println("DEBUG: FAILED. Only found: " + found + "/" + totalRequired); + return false; + } + + /** + * Get the screen position for the recipe book toggle button. + * + * @return the ScreenPosition located 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical centre + */ + @Override + protected ScreenPosition getRecipeBookButtonPosition() { + // 1. Calculate the start (left) of your workbench GUI + int guiLeft = (this.width - this.imageWidth) / 2; + + // 2. Calculate the top of your workbench GUI + int guiTop = (this.height - this.imageHeight) / 2; + + // 3. Standard Vanilla positioning: + // Usually 5 pixels in from the left and 49 pixels up from the center + return new ScreenPosition(guiLeft + 5, guiTop + this.imageHeight / 2 - 49); + } +} \ No newline at end of file diff --git a/src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java index e628c65..3eef9c6 100644 --- a/src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java @@ -9,7 +9,6 @@ import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; import com.tcm.MineTale.block.workbenches.menu.WorkbenchWorkbenchMenu; import com.tcm.MineTale.mixin.client.ClientRecipeBookAccessor; -import com.tcm.MineTale.mixin.client.RecipeBookComponentAccessor; import com.tcm.MineTale.network.CraftRequestPayload; import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; import com.tcm.MineTale.registry.ModBlocks; @@ -23,7 +22,6 @@ import net.minecraft.client.gui.navigation.ScreenPosition; import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; -import net.minecraft.client.gui.screens.recipebook.RecipeCollection; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.core.Holder; import net.minecraft.resources.Identifier; @@ -43,6 +41,8 @@ public class WorkbenchWorkbenchScreen extends AbstractRecipeBookScreen results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); - - if (!results.isEmpty()) { - ItemStack resultStack = results.get(0); - - // 4. LOG FOR DEBUGGING - System.out.println("Sending craft request for: " + resultStack + " amount: " + amount); - - ClientPlayNetworking.send(new CraftRequestPayload(resultStack, amount)); - } - break; + // Look at our "Memory" instead of the component + if (this.lastKnownSelectedId != null) { + ClientRecipeBook book = this.minecraft.player.getRecipeBook(); + RecipeDisplayEntry entry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); + + if (entry != null) { + List results = entry.resultItems(SlotDisplayContext.fromLevel(this.minecraft.level)); + if (!results.isEmpty()) { + System.out.println("Persistent Selection Success: " + results.get(0)); + ClientPlayNetworking.send(new CraftRequestPayload(results.get(0), amount)); + return; } } - } else { - System.out.println("Request failed: Collection or DisplayID is null!"); } + System.out.println("Request failed: No recipe was ever selected!"); } /** - * Draws the workbench GUI background texture at the screen's top-left corner. + * Draws the workbench background texture at the screen's current GUI origin. * * @param guiGraphics the graphics context used to draw GUI elements * @param f partial tick time for interpolation - * @param i current mouse x coordinate relative to the window - * @param j current mouse y coordinate relative to the window + * @param i current mouse x coordinate + * @param j current mouse y coordinate */ protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { int k = this.leftPos; @@ -172,20 +157,36 @@ protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { guiGraphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, k, l, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256); } + /** + * Render the workbench screen, persist the current recipe selection and update craft-button availability. + * + * Persists the recipe selected in the recipe book (if any), resolves that remembered selection against the client's known recipes, + * enables or disables the craft buttons according to whether the player has sufficient ingredients for counts of 1, 2 and 10, + * and renders the background, UI and tooltips. + * + * @param graphics the GUI graphics context used for rendering + * @param mouseX current mouse X position + * @param mouseY current mouse Y position + * @param delta frame delta time in ticks + */ @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { renderBackground(graphics, mouseX, mouseY, delta); super.render(graphics, mouseX, mouseY, delta); - // Get the ID of the recipe clicked in the ghost-book - RecipeDisplayId displayId = this.mineTaleRecipeBook.getSelectedRecipeId(); - RecipeDisplayEntry selectedEntry = null; + // 1. Get the current selection from the book + RecipeDisplayId currentId = this.mineTaleRecipeBook.getSelectedRecipeId(); + + // 2. If it's NOT null, remember it! + if (currentId != null) { + this.lastKnownSelectedId = currentId; + } - if (displayId != null && this.minecraft.level != null) { + // 3. Use the remembered ID to find the entry for button activation + RecipeDisplayEntry selectedEntry = null; + if (this.lastKnownSelectedId != null && this.minecraft.level != null) { ClientRecipeBook book = this.minecraft.player.getRecipeBook(); - // Accessing the known recipes via your Accessor - Map knownRecipes = ((ClientRecipeBookAccessor) book).getKnown(); - selectedEntry = knownRecipes.get(displayId); + selectedEntry = ((ClientRecipeBookAccessor) book).getKnown().get(this.lastKnownSelectedId); } // 2. Button Activation Logic diff --git a/src/client/java/com/tcm/MineTale/datagen/recipes/BuilderRecipes.java b/src/client/java/com/tcm/MineTale/datagen/recipes/BuilderRecipes.java index 2c2a960..9ac0758 100644 --- a/src/client/java/com/tcm/MineTale/datagen/recipes/BuilderRecipes.java +++ b/src/client/java/com/tcm/MineTale/datagen/recipes/BuilderRecipes.java @@ -1,28 +1,43 @@ package com.tcm.MineTale.datagen.recipes; +import com.tcm.MineTale.datagen.builders.WorkbenchRecipeBuilder; +import com.tcm.MineTale.registry.ModBlocks; +import com.tcm.MineTale.registry.ModItems; +import com.tcm.MineTale.registry.ModRecipeDisplay; +import com.tcm.MineTale.registry.ModRecipes; + import net.minecraft.core.HolderLookup; import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.data.recipes.RecipeProvider; public class BuilderRecipes { + /** + * Register workbench recipes for the builders mod used by the data generator. + * + * Creates and saves two recipes (rope and diagonal rope), each requiring plant fibre, + * taking 3 ticks, unlocked when the player has the builders workbench, and assigned + * to the builders recipe book category. + * + * @param provider the recipe provider used to form unlock conditions + * @param exporter the recipe output to which generated recipes are written + * @param lookup registry holder lookup provider used for resolving required holders + */ public static void buildRecipes(RecipeProvider provider, RecipeOutput exporter, HolderLookup.Provider lookup) { - // TODO: BUILDERS_WORKBENCH_BLOCK & ROPE Not Implemented - // new WorkbenchRecipeBuilder(ModRecipes.BUILDER_TYPE, ModRecipes.BUILDER_SERIALIZER) - // .input(ModItems.PLANT_FIBER) - // .output(ModBlocks.ROPE.asItem()) - // .time(3) - // .unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BUILDER_SEARCH) - // .save(exporter, "builders_workbench_rope"); + new WorkbenchRecipeBuilder(ModRecipes.BUILDERS_TYPE, ModRecipes.BUILDERS_SERIALIZER) + .input(ModItems.PLANT_FIBER) + .output(ModBlocks.ROPE) + .time(3) + .unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK)) + .bookCategory(ModRecipeDisplay.BUILDERS_SEARCH) + .save(exporter, "builders_workbench_rope"); - // TODO: BUILDERS_WORKBENCH_BLOCK & ROPE_DIAGONAL Not Implemented - // new WorkbenchRecipeBuilder(ModRecipes.BUILDER_TYPE, ModRecipes.BUILDER_SERIALIZER) - // .input(ModItems.PLANT_FIBER) - // .output(ModBlocks.ROPE_DIAGONAL.asItem()) - // .time(3) - // .unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK.asItem())) - // .bookCategory(ModRecipeDisplay.BUILDER_SEARCH) - // .save(exporter, "builders_workbench_rope_diagonal"); + new WorkbenchRecipeBuilder(ModRecipes.BUILDERS_TYPE, ModRecipes.BUILDERS_SERIALIZER) + .input(ModItems.PLANT_FIBER) + .output(ModBlocks.ROPE_DIAGONAL) + .time(3) + .unlockedBy("has_builders_workbench", provider.has(ModBlocks.BUILDERS_WORKBENCH_BLOCK)) + .bookCategory(ModRecipeDisplay.BUILDERS_SEARCH) + .save(exporter, "builders_workbench_rope_diagonal"); } } diff --git a/src/main/java/com/tcm/MineTale/MineTale.java b/src/main/java/com/tcm/MineTale/MineTale.java index 44dc877..b3c4734 100644 --- a/src/main/java/com/tcm/MineTale/MineTale.java +++ b/src/main/java/com/tcm/MineTale/MineTale.java @@ -18,8 +18,6 @@ import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; import com.tcm.MineTale.block.workbenches.menu.AbstractWorkbenchContainerMenu; -import com.tcm.MineTale.block.workbenches.menu.ArmorersWorkbenchMenu; -import com.tcm.MineTale.block.workbenches.menu.WorkbenchWorkbenchMenu; import com.tcm.MineTale.network.ClientboundNearbyInventorySyncPacket; import com.tcm.MineTale.network.CraftRequestPayload; import com.tcm.MineTale.recipe.WorkbenchRecipe; @@ -42,10 +40,12 @@ public class MineTale implements ModInitializer { public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); /** - * Initializes and registers the mod's game content and subsystems during Fabric startup. + * Initialises the mod by registering game content, networking codecs and runtime subsystems. * - *

Triggers initialization for blocks, block entities, menu types, entities, items, and entity - * data serializers so they are registered with the game before gameplay begins.

+ * Performs startup registrations in dependency order (blocks, items, block entities, entities, + * menus and recipes), registers creative-tab and entity data serializers, applies loot-table + * modifiers, synchronises the furnace recipe serializer, and registers client↔server payload codecs + * plus a global server receiver that handles craft requests from workbench-like menus. */ @Override public void onInitialize() { @@ -81,80 +81,26 @@ public void onInitialize() { PayloadTypeRegistry.playS2C().register(ClientboundNearbyInventorySyncPacket.TYPE, ClientboundNearbyInventorySyncPacket.STREAM_CODEC); - // Register the server-side receiver using .TYPE - // ServerPlayNetworking.registerGlobalReceiver(CraftRequestPayload.TYPE, (payload, context) -> { - // context.server().execute(() -> { - // ServerPlayer player = context.player(); - - // // --- SECURITY GUARD --- - // // Ensure the player actually has the Workbench UI open before processing the craft - // if (!(player.containerMenu instanceof WorkbenchWorkbenchMenu)) { - // return; - // } - - // ItemStack requestedResult = payload.resultItem(); - // int amount = payload.amount(); - - // // 1. Get the RecipeManager from the server level - // RecipeManager recipeManager = player.level().recipeAccess(); - - // // 2. Find the recipe by matching the output ItemStack - // Optional> recipeOpt = recipeManager.getAllOfType(ModRecipes.WORKBENCH_TYPE).stream() - // .filter(holder -> { - // // Guard against recipes with no results before accessing index 0 - // if (holder.value().results().isEmpty()) { - // return false; - // } - - // // Compare the first result of the workbench recipe to the requested item - // ItemStack result = holder.value().results().get(0); - // return ItemStack.isSameItem(result, requestedResult); - // }) - // .findFirst(); - - // if (recipeOpt.isPresent()) { - // WorkbenchRecipe recipe = recipeOpt.get().value(); - - // // 2. Determine craft limit (Handle "All" logic) - // int limit = (amount == -1) ? 64 : Math.min(Math.max(amount, 0), 64); - - // for (int i = 0; i < limit; i++) { - // if (hasIngredients(player, recipe)) { - // consumeIngredients(player, recipe); - // player.getInventory().add(recipe.results().get(0).copy()); - // } else { - // break; - // } - // } - - // // 3. Sync inventory changes to the client screen - // player.containerMenu.broadcastChanges(); - // } - // }); - // }); - ServerPlayNetworking.registerGlobalReceiver(CraftRequestPayload.TYPE, (payload, context) -> { context.server().execute(() -> { ServerPlayer player = context.player(); // --- SELECTIVE SECURITY GUARD --- // Only proceed if the menu is one of the two specific workbenches - boolean isWorkbench = player.containerMenu instanceof WorkbenchWorkbenchMenu; - boolean isArmorers = player.containerMenu instanceof ArmorersWorkbenchMenu; + boolean isWorkbench = player.containerMenu instanceof AbstractWorkbenchContainerMenu; - if (!isWorkbench && !isArmorers) { + if (!isWorkbench) { return; // Reject packets from Campfires, Furnaces, or other menus } + AbstractWorkbenchContainerMenu instanceContainerMenu = (AbstractWorkbenchContainerMenu) player.containerMenu; + ItemStack requestedResult = payload.resultItem(); int amount = payload.amount(); RecipeManager recipeManager = player.level().recipeAccess(); - // 1. Determine which Recipe Type to search based on the open menu - var targetType = isWorkbench ? ModRecipes.WORKBENCH_TYPE : ModRecipes.ARMORERS_TYPE; - // 2. Find the recipe within that specific type - Optional> recipeOpt = recipeManager.getAllOfType(targetType).stream() + Optional> recipeOpt = recipeManager.getAllOfType(instanceContainerMenu.getRecipeType()).stream() .filter(holder -> { if (holder.value().results().isEmpty()) return false; diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/BuildersWorkbench.java b/src/main/java/com/tcm/MineTale/block/workbenches/BuildersWorkbench.java new file mode 100644 index 0000000..9c3ee86 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/BuildersWorkbench.java @@ -0,0 +1,96 @@ +package com.tcm.MineTale.block.workbenches; + +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; + +import com.mojang.serialization.MapCodec; +import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; +import com.tcm.MineTale.block.workbenches.entity.BuildersWorkbenchEntity; +import com.tcm.MineTale.registry.ModBlockEntities; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class BuildersWorkbench extends AbstractWorkbench { + public static final boolean IS_WIDE = true; + public static final boolean IS_TALL = false; + + public static final MapCodec CODEC = simpleCodec(BuildersWorkbench::new); + + /** + * Create a BuildersWorkbench that uses the mod's BUILDERS_WORKBENCH_BE block entity type. + * + * @param properties block properties to apply to this workbench + */ + public BuildersWorkbench(Properties properties) { + // Hardcode the supplier and sounds here if they never change + super(properties, () -> ModBlockEntities.BUILDERS_WORKBENCH_BE, IS_WIDE, IS_TALL, 1); + } + + /** + * Constructs a BuildersWorkbench using the provided block properties and block-entity type supplier. + * + * @param properties block properties to apply to this workbench + * @param supplier supplier that provides the BlockEntityType for the BuildersWorkbenchEntity + */ + public BuildersWorkbench(Properties properties, Supplier> supplier) { + super(properties, supplier, IS_WIDE, IS_TALL, 1); + } + + /** + * Provides a ticker for workbench block entities when the supplied block entity type matches this block's entity type. + * + * @param type the block entity type to match against this block's workbench entity type + * @return a BlockEntityTicker that updates matching workbench block entities, or {@code null} if the types do not match + */ + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + // This connects the Level's ticking system to your static tick method + return createTickerHelper(type, ModBlockEntities.BUILDERS_WORKBENCH_BE, AbstractWorkbenchEntity::tick); + } + + /** + * Provides the MapCodec used to serialize and deserialize this workbench. + * + * @return the MapCodec for this BuildersWorkbench + */ + @Override + protected MapCodec codec() { + return CODEC; + } + + /** + * Specifies that this block is rendered using its block model. + * + * @return RenderShape.MODEL to render the block using its JSON/model representation. + */ + @Override + public RenderShape getRenderShape(BlockState state) { + // BaseEntityBlock defaults to INVISIBLE. + // We set it to MODEL so the JSON model is rendered. + return RenderShape.MODEL; + } + + private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 16, 16); + + /** + * The block's collision and interaction shape as a 1×1 footprint (x 0–16, y 0–16, z 0–16). + * + * @return the voxel shape used for collision and interaction + */ + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/FarmersWorkbench.java b/src/main/java/com/tcm/MineTale/block/workbenches/FarmersWorkbench.java new file mode 100644 index 0000000..3bfd8b0 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/FarmersWorkbench.java @@ -0,0 +1,98 @@ +package com.tcm.MineTale.block.workbenches; + +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; + +import com.mojang.serialization.MapCodec; +import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; +import com.tcm.MineTale.block.workbenches.entity.FarmersWorkbenchEntity; +import com.tcm.MineTale.registry.ModBlockEntities; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +// ChestBlock + +public class FarmersWorkbench extends AbstractWorkbench { + public static final boolean IS_WIDE = true; + public static final boolean IS_TALL = false; + + public static final MapCodec CODEC = simpleCodec(FarmersWorkbench::new); + + /** + * Initialise a FarmersWorkbench that uses the mod's FARMERS_WORKBENCH_BE block-entity type and default size. + * + * @param properties block properties for this workbench + */ + public FarmersWorkbench(Properties properties) { + // Hardcode the supplier and sounds here if they never change + super(properties, () -> ModBlockEntities.FARMERS_WORKBENCH_BE, IS_WIDE, IS_TALL, 1); + } + + /** + * Constructs a FarmersWorkbench with the supplied block properties and block-entity type supplier. + * + * @param properties block properties to apply to this workbench + * @param supplier supplier of the BlockEntityType to use for the workbench's block entity + */ + public FarmersWorkbench(Properties properties, Supplier> supplier) { + super(properties, supplier, IS_WIDE, IS_TALL, 1); + } + + /** + * Provides a ticker for workbench block entities when the supplied block entity type matches this block's entity type. + * + * @param type the block entity type to match against this block's workbench entity type + * @return a BlockEntityTicker that updates matching workbench block entities, or {@code null} if the types do not match + */ + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + // This connects the Level's ticking system to your static tick method + return createTickerHelper(type, ModBlockEntities.FARMERS_WORKBENCH_BE, AbstractWorkbenchEntity::tick); + } + + /** + * Provides the MapCodec used to serialize and deserialize this workbench. + * + * @return the MapCodec for this FarmersWorkbench + */ + @Override + protected MapCodec codec() { + return CODEC; + } + + /** + * Specifies that this block is rendered using its block model. + * + * @return RenderShape.MODEL to render the block using its JSON/model representation. + */ + @Override + public RenderShape getRenderShape(BlockState state) { + // BaseEntityBlock defaults to INVISIBLE. + // We set it to MODEL so the JSON model is rendered. + return RenderShape.MODEL; + } + + private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 16, 16); + + /** + * Provide the block's collision and interaction shape as a full 1×1×1 footprint. + * + * @return a VoxelShape representing a full 1×1×1 block (x 0–16, y 0–16, z 0–16) + */ + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/entity/BuildersWorkbenchEntity.java b/src/main/java/com/tcm/MineTale/block/workbenches/entity/BuildersWorkbenchEntity.java new file mode 100644 index 0000000..e1dfd21 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/entity/BuildersWorkbenchEntity.java @@ -0,0 +1,180 @@ +package com.tcm.MineTale.block.workbenches.entity; + +import java.util.ArrayList; +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import com.mojang.serialization.Codec; +import com.tcm.MineTale.block.workbenches.menu.BuildersWorkbenchMenu; +import com.tcm.MineTale.recipe.WorkbenchRecipe; +import com.tcm.MineTale.registry.ModBlockEntities; +import com.tcm.MineTale.registry.ModRecipes; +import com.tcm.MineTale.util.Constants; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +public class BuildersWorkbenchEntity extends AbstractWorkbenchEntity { + protected final ContainerData data = new ContainerData() { + /** + * Retrieve an internal data field for container UI synchronization. + * + * @param index index of the data field (0–3) + * @return the value of the data field; always `0` + */ + @Override + public int get(int index) { + return switch (index) { + default -> 0; + }; + } + + /** + * Ignored setter for client-side code; workbench data is driven by the server. + * + * Both parameters are intentionally ignored. + * + * @param index the data index (ignored) + * @param value the value to assign (ignored) + */ + @Override + public void set(int index, int value) { + // Not required on WorkbenchEntity + } + + /** + * The number of data values exposed by this ContainerData. + * + * @return the number of data entries (4) + */ + @Override + public int getCount() { + return 4; + } + }; + + /** + * Constructs a BuildersWorkbenchEntity at the given world position and block state. + * + * Initialises the workbench tier to 1 and enables pulling items from nearby blocks. + * + * @param blockPos the world position of this block entity + * @param blockState the block state for this block entity + */ + public BuildersWorkbenchEntity(BlockPos blockPos, BlockState blockState) { + super(ModBlockEntities.BUILDERS_WORKBENCH_BE, blockPos, blockState); + + this.tier = 1; + this.canPullFromNearby = true; + } + + /** + * Persist the workbench's state into the provided ValueOutput. + * + * Stores the following entries: + * - "WorkbenchTier" as an int + * - "ScanRadius" as a double + * - "Inventory" as a List (using ItemStack codecs) + * + * @param valueOutput the writer used to serialise this entity's fields + */ + @Override + protected void saveAdditional(ValueOutput valueOutput) { + super.saveAdditional(valueOutput); + // store() uses Codecs for type safety + valueOutput.store("WorkbenchTier", Codec.INT, this.tier); + valueOutput.store("ScanRadius", Codec.DOUBLE, this.scanRadius); + + // Convert the SimpleContainer to a List of ItemStacks for the Codec + // Or use the built-in NBT helper if your framework supports it + List stacks = new ArrayList<>(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + stacks.add(inventory.getItem(i)); + } + + // CHANGE: Use OPTIONAL_CODEC instead of CODEC + valueOutput.store("Inventory", ItemStack.OPTIONAL_CODEC.listOf(), stacks); + } + + /** + * Restores workbench-specific state from persistent storage, applying defaults when keys are absent. + * + * Delegates to the superclass load logic, then: + * - reads "WorkbenchTier" (int) into {@code tier}, defaulting to {@code 1} if missing; + * - reads "ScanRadius" (double) into {@code scanRadius}, defaulting to {@code 0.0} if missing; + * - reads "Inventory" as a list of {@code ItemStack} and populates the internal inventory up to its capacity. + */ + @Override + protected void loadAdditional(ValueInput valueInput) { + super.loadAdditional(valueInput); + // read() returns an Optional + this.tier = valueInput.read("WorkbenchTier", Codec.INT).orElse(1); + this.scanRadius = valueInput.read("ScanRadius", Codec.DOUBLE).orElse(0.0); + + // Read the inventory list back + valueInput.read("Inventory", ItemStack.OPTIONAL_CODEC.listOf()).ifPresent(stacks -> { + for (int i = 0; i < stacks.size() && i < inventory.getContainerSize(); i++) { + inventory.setItem(i, stacks.get(i)); + } + }); + } + + /** + * Creates the server-side container menu for this workbench's UI. + * + * @param syncId the window id used to synchronize the menu with the client + * @param playerInventory the opening player's inventory + * @param player the player who opened the menu + * @return a BuildersWorkbenchMenu bound to this workbench's inventory and synced data + */ + @Override + public @Nullable AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) { + // 1. Trigger the sync to the client before returning the menu + if (player instanceof ServerPlayer serverPlayer) { + this.syncNearbyToPlayer(serverPlayer); + } + + // // 2. Return the menu as usual + return new BuildersWorkbenchMenu(syncId, playerInventory, this.data, this); + } + + /** + * Identifies the recipe type used to find and match recipes for this workbench. + * + * @return the RecipeType for workbench recipes + */ + @Override + public RecipeType getWorkbenchRecipeType() { + return ModRecipes.BUILDERS_TYPE; + } + + /** + * Determines whether the workbench currently has fuel available. + * + * Checks that the entity is in a loaded level and that the configured fuel slot contains an item. + * + * @return true if the entity is in a loaded level and the fuel slot contains an item, false otherwise. + */ + @Override + protected boolean hasFuel() { + if (this.level == null) return false; + + // Check if block is lit + // BlockState state = this.level.getBlockState(this.worldPosition); + // boolean isLit = state.hasProperty(BlockStateProperties.LIT) && state.getValue(BlockStateProperties.LIT); + + boolean hasFuelItem = !this.getItem(Constants.FUEL_SLOT).isEmpty(); + + return hasFuelItem; + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/ArmorersWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/ArmorersWorkbenchMenu.java index 313423a..2803e6b 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/menu/ArmorersWorkbenchMenu.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/ArmorersWorkbenchMenu.java @@ -7,6 +7,7 @@ import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; import com.tcm.MineTale.recipe.WorkbenchRecipeInput; import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; @@ -36,13 +37,14 @@ public ArmorersWorkbenchMenu(int syncId, Inventory playerInventory) { } /** - * Creates a workbench menu associated with the given player inventory and optional block entity. + * Initialise a workbench menu bound to the given player inventory and optional block entity. * - * Uses an empty internal container (size 0) and the class's data size for syncing numeric state. + * Uses an empty internal container (size 0) and the class DATA_SIZE constant to synchronise numeric state + * between server and client. * - * @param syncId synchronization id for this menu + * @param syncId the synchronisation id for this menu * @param playerInventory the player's inventory used for slot access and recipe-book integration - * @param data container data used to sync numeric state between server and client + * @param data container data used to synchronise numeric state between server and client * @param blockEntity nullable block entity this menu is bound to, or {@code null} if not bound */ public ArmorersWorkbenchMenu(int syncId, Inventory playerInventory, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { @@ -56,7 +58,8 @@ public ArmorersWorkbenchMenu(int syncId, Inventory playerInventory, ContainerDat DATA_SIZE, playerInventory, EMPTY_SIZE, - EMPTY_SIZE + EMPTY_SIZE, + ModRecipes.ARMORERS_TYPE ); this.blockEntity = blockEntity; this.playerInventory = playerInventory; @@ -73,27 +76,13 @@ public ArmorersWorkbenchMenu(int syncId, Inventory playerInventory, ContainerDat } /** - * Populate the given StackedItemContents with the items available through this menu for recipe-book calculations. + * Populate a StackedItemContents with item stacks available for crafting lookups. + * + * This accounts for stacks in the player's inventory, any stacks held in this menu's + * internal container slots, and nearby item stacks provided via networked data when present. * - * @param stackedItemContents container to receive consolidated item counts from the menu's inventories + * @param contents the StackedItemContents to populate with accounted stacks */ - // @Override - // public void fillCraftSlotsStackedContents(StackedItemContents stackedItemContents) { - // // 1. Tell the book the player has items in their pockets - // this.playerInventory.fillStackedContents(stackedItemContents); - - // // 2. Tell the book the "Nearby Chests" items also count - // AbstractWorkbenchEntity be = this.getBlockEntity(); - // if (be != null && be.isCanPullFromNearby()) { - // // This runs on the CLIENT UI, making the icons turn WHITE - // for (Container nearby : be.getNearbyInventories()) { - // for (int i = 0; i < nearby.getContainerSize(); i++) { - // stackedItemContents.accountStack(nearby.getItem(i)); - // } - // } - // } - // } - @Override public void fillCraftSlotsStackedContents(StackedItemContents contents) { // 1. Account for items in the player's pockets diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/BuildersWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/BuildersWorkbenchMenu.java new file mode 100644 index 0000000..02c4fbc --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/BuildersWorkbenchMenu.java @@ -0,0 +1,132 @@ +package com.tcm.MineTale.block.workbenches.menu; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; +import com.tcm.MineTale.recipe.WorkbenchRecipeInput; +import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; + +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.RecipeBookType; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +public class BuildersWorkbenchMenu extends AbstractWorkbenchContainerMenu { + // No internal inventory needed anymore, but we pass an empty container to the super + private static final int EMPTY_SIZE = 0; + private static final int DATA_SIZE = 0; + + @Nullable + private final AbstractWorkbenchEntity blockEntity; + private final Inventory playerInventory; + + /** + * Construct a client-side BuildersWorkbenchMenu used when the workbench UI opens. + * + * @param syncId synchronization id that pairs this menu with the server + * @param playerInventory the player's inventory to bind to this menu + */ + public BuildersWorkbenchMenu(int syncId, Inventory playerInventory) { + this(syncId, playerInventory, new SimpleContainerData(EMPTY_SIZE), null); + } + + /** + * Creates a workbench menu associated with the given player inventory and optional block entity. + * + * Uses an empty internal container (size 0) and the class's data size for syncing numeric state. + * + * @param syncId synchronization id for this menu + * @param playerInventory the player's inventory used for slot access and recipe-book integration + * @param data container data used to sync numeric state between server and client + * @param blockEntity nullable block entity this menu is bound to, or {@code null} if not bound + */ + public BuildersWorkbenchMenu(int syncId, Inventory playerInventory, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { + // Note: The order of arguments depends on your AbstractWorkbenchContainerMenu, + // but the 'expectedSize' parameter MUST be 0. + super( + ModMenuTypes.BUILDERS_WORKBENCH_MENU, + syncId, + new SimpleContainer(EMPTY_SIZE), + data, + DATA_SIZE, + playerInventory, + EMPTY_SIZE, + EMPTY_SIZE, + ModRecipes.BUILDERS_TYPE + ); + this.blockEntity = blockEntity; + this.playerInventory = playerInventory; + } + + /** + * Accesses the block entity bound to this menu, if present. + * + * @return the bound AbstractWorkbenchEntity, or {@code null} if this menu is not bound to a block entity + */ + @Override + public @Nullable AbstractWorkbenchEntity getBlockEntity() { + return this.blockEntity; + } + + /** + * Populate the provided StackedItemContents with all item stacks available to this workbench for recipe matching. + * + * This includes items from the player's inventory, any items present in the menu's internal container, and + * network-synchronised nearby items supplied by the server. + * + * @param contents the StackedItemContents to populate for recipe book matching + */ + @Override + public void fillCraftSlotsStackedContents(StackedItemContents contents) { + // 1. Account for items in the player's pockets + this.playerInventory.fillStackedContents(contents); + + // 2. Account for items sitting in the Workbench slots (if any) + for (int i = 0; i < this.container.getContainerSize(); i++) { + contents.accountStack(this.container.getItem(i)); + } + + // 3. THE FIX: Use the list provided by the Packet (Networked Items) + // We stop calling be.getNearbyInventories() here because it returns empty on Client + List nearbyItems = this.getNetworkedNearbyItems(); + + if (!nearbyItems.isEmpty() && this.playerInventory.player.level().isClientSide()) { + System.out.println("DEBUG: Recipe Book is now accounting for " + nearbyItems.size() + " stacks from the packet!"); + } + + for (ItemStack stack : nearbyItems) { + contents.accountStack(stack); + } + } + + /** + * Selects the crafting recipe-book category for this menu. + * + * @return {@code RecipeBookType.CRAFTING} + */ + @Override + public RecipeBookType getRecipeBookType() { + // This keeps the Crafting-style recipe book available on the UI + return RecipeBookType.CRAFTING; + } + + /** + * Create a recipe input representing no items for this menu. + * + * This menu has no internal crafting slots, so the input is empty. + * + * @return a WorkbenchRecipeInput with both input stacks set to ItemStack.EMPTY + */ + @Override + public WorkbenchRecipeInput createRecipeInput() { + // Since there are no slots, we return an empty input. + // The actual crafting logic will scan the player inventory directly when a button is clicked. + return new WorkbenchRecipeInput(ItemStack.EMPTY, ItemStack.EMPTY); + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/CampfireWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/CampfireWorkbenchMenu.java index 0245b26..2b37ad1 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/menu/CampfireWorkbenchMenu.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/CampfireWorkbenchMenu.java @@ -5,6 +5,7 @@ import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; import com.tcm.MineTale.recipe.WorkbenchRecipeInput; import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; import com.tcm.MineTale.util.Constants; import net.minecraft.world.Container; @@ -23,12 +24,11 @@ public class CampfireWorkbenchMenu extends AbstractWorkbenchContainerMenu { private final AbstractWorkbenchEntity blockEntity; /** - * Creates a CampfireWorkbenchMenu using default internal storage and data containers. + * Creates a CampfireWorkbenchMenu initialised with the default internal inventory and data containers. * - * Constructs a menu with a new SimpleContainer of size {@code containerSize} and a new - * SimpleContainerData of size {@code containerDataSize}, then delegates to the primary constructor. + * The menu uses a 7-slot internal container and a data container sized by {@code containerDataSize}. * - * @param syncId synchronization id for the menu + * @param syncId the synchronisation id for this menu * @param playerInventory the player's inventory interacting with this menu */ public CampfireWorkbenchMenu(int syncId, Inventory playerInventory) { @@ -36,15 +36,16 @@ public CampfireWorkbenchMenu(int syncId, Inventory playerInventory) { } /** - * Creates a CampfireWorkbenchMenu bound to the given player inventory, container, and container data. + * Creates a CampfireWorkbenchMenu bound to the supplied player inventory, container and container data. * - * @param syncId the synchronization id for this menu (used by the client/server container sync) + * @param syncId the synchronisation id used for client–server container syncing * @param playerInventory the player's inventory - * @param container the backing container for the workbench slots - * @param data the container data used for syncing additional numeric state + * @param container the backing container that provides the workbench slots + * @param data the container data used to synchronise numeric state + * @param blockEntity the associated workbench block entity, or {@code null} if the menu is not bound to a block */ public CampfireWorkbenchMenu(int syncId, Inventory playerInventory, Container container, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { - super(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, syncId, container, data, containerDataSize, playerInventory, Constants.INPUT_START + 1, 6); + super(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, syncId, container, data, containerDataSize, playerInventory, Constants.INPUT_START + 1, 6, ModRecipes.CAMPFIRE_TYPE); this.blockEntity = blockEntity; } diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/FarmersWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/FarmersWorkbenchMenu.java new file mode 100644 index 0000000..5a4bb36 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/FarmersWorkbenchMenu.java @@ -0,0 +1,129 @@ +package com.tcm.MineTale.block.workbenches.menu; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; +import com.tcm.MineTale.recipe.WorkbenchRecipeInput; +import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; + +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.RecipeBookType; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +public class FarmersWorkbenchMenu extends AbstractWorkbenchContainerMenu { + // No internal inventory needed anymore, but we pass an empty container to the super + private static final int EMPTY_SIZE = 0; + private static final int DATA_SIZE = 0; + + @Nullable + private final AbstractWorkbenchEntity blockEntity; + private final Inventory playerInventory; + + /** + * Create a client-side FarmersWorkbenchMenu with no bound block entity and empty container data. + * + * @param syncId the menu synchronization id that pairs this client menu with its server counterpart + * @param playerInventory the player's inventory to bind to the menu + */ + public FarmersWorkbenchMenu(int syncId, Inventory playerInventory) { + this(syncId, playerInventory, new SimpleContainerData(EMPTY_SIZE), null); + } + + /** + * Initialise a Farmers workbench menu bound to a player's inventory and an optional block entity. + * + * Uses an empty internal container and the menu's container data for server–client numeric syncing. + * + * @param syncId synchronization id for this menu + * @param playerInventory the player's inventory used for slot access and recipe-book integration + * @param data container data used to sync numeric state between server and client + * @param blockEntity the block entity this menu is bound to, or {@code null} if not bound + */ + public FarmersWorkbenchMenu(int syncId, Inventory playerInventory, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { + // Note: The order of arguments depends on your AbstractWorkbenchContainerMenu, + // but the 'expectedSize' parameter MUST be 0. + super( + ModMenuTypes.FARMERS_WORKBENCH_MENU, + syncId, + new SimpleContainer(EMPTY_SIZE), + data, + DATA_SIZE, + playerInventory, + EMPTY_SIZE, + EMPTY_SIZE, + ModRecipes.FARMERS_TYPE + ); + this.blockEntity = blockEntity; + this.playerInventory = playerInventory; + } + + /** + * Accesses the block entity bound to this menu, if present. + * + * @return the bound AbstractWorkbenchEntity, or {@code null} if this menu is not bound to a block entity + */ + @Override + public @Nullable AbstractWorkbenchEntity getBlockEntity() { + return this.blockEntity; + } + + /** + * Populates the provided StackedItemContents with item stacks available for recipe matching. + * + * This accounts for items from the player's inventory, any items stored in the menu's internal container slots, and nearby item stacks supplied via the menu's networked list (used by clients). + * + * @param contents accumulator that will receive accounted item stacks for recipe lookup + */ + @Override + public void fillCraftSlotsStackedContents(StackedItemContents contents) { + // 1. Account for items in the player's pockets + this.playerInventory.fillStackedContents(contents); + + // 2. Account for items sitting in the Workbench slots (if any) + for (int i = 0; i < this.container.getContainerSize(); i++) { + contents.accountStack(this.container.getItem(i)); + } + + // 3. THE FIX: Use the list provided by the Packet (Networked Items) + // We stop calling be.getNearbyInventories() here because it returns empty on Client + List nearbyItems = this.getNetworkedNearbyItems(); + + if (!nearbyItems.isEmpty() && this.playerInventory.player.level().isClientSide()) { + System.out.println("DEBUG: Recipe Book is now accounting for " + nearbyItems.size() + " stacks from the packet!"); + } + + for (ItemStack stack : nearbyItems) { + contents.accountStack(stack); + } + } + + /** + * Selects the crafting recipe-book category for this menu. + * + * @return {@code RecipeBookType.CRAFTING} + */ + @Override + public RecipeBookType getRecipeBookType() { + // This keeps the Crafting-style recipe book available on the UI + return RecipeBookType.CRAFTING; + } + + /** + * Create an empty recipe input with both input stacks empty. + * + * @return a WorkbenchRecipeInput with both input stacks set to ItemStack.EMPTY + */ + @Override + public WorkbenchRecipeInput createRecipeInput() { + // Since there are no slots, we return an empty input. + // The actual crafting logic will scan the player inventory directly when a button is clicked. + return new WorkbenchRecipeInput(ItemStack.EMPTY, ItemStack.EMPTY); + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/FurnaceWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/FurnaceWorkbenchMenu.java index b12b33f..597b611 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/menu/FurnaceWorkbenchMenu.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/FurnaceWorkbenchMenu.java @@ -5,6 +5,7 @@ import com.tcm.MineTale.block.workbenches.entity.AbstractFurnaceWorkbenchEntity; import com.tcm.MineTale.recipe.WorkbenchRecipeInput; import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; import com.tcm.MineTale.util.Constants; import net.minecraft.world.Container; @@ -23,7 +24,7 @@ public class FurnaceWorkbenchMenu extends AbstractWorkbenchContainerMenu { private final AbstractFurnaceWorkbenchEntity blockEntity; /** - * Creates a client-side FurnaceWorkbenchMenu with a new internal container and container data. + * Creates a client-side FurnaceWorkbenchMenu backed by a new internal container and default container data. * * @param syncId the window synchronization id assigned by the client * @param playerInventory the player's inventory to attach to this menu @@ -32,8 +33,17 @@ public FurnaceWorkbenchMenu(int syncId, Inventory playerInventory) { this(syncId, playerInventory, new SimpleContainer(7), new SimpleContainerData(containerDataSize), null); } + /** + * Creates a furnace workbench menu bound to the given player inventory, backing container, container data and optional block entity. + * + * @param syncId the window sync id used for client–server menu synchronisation + * @param playerInventory the player's inventory displayed and managed by this menu + * @param container the backing container holding the menu's slot items + * @param data container data used to sync menu state (for example progress fields) + * @param blockEntity the associated AbstractFurnaceWorkbenchEntity, or {@code null} when constructed client-side + */ public FurnaceWorkbenchMenu(int syncId, Inventory playerInventory, Container container, ContainerData data, @Nullable AbstractFurnaceWorkbenchEntity blockEntity) { - super(ModMenuTypes.FURNACE_WORKBENCH_MENU, syncId, container, data, containerDataSize, playerInventory, Constants.INPUT_START + 1, 6); + super(ModMenuTypes.FURNACE_WORKBENCH_MENU, syncId, container, data, containerDataSize, playerInventory, Constants.INPUT_START + 1, 6, ModRecipes.FURNACE_T1_TYPE); this.blockEntity = blockEntity; } diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/WorkbenchWorkbenchMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/WorkbenchWorkbenchMenu.java index 7b8fe06..6922040 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/menu/WorkbenchWorkbenchMenu.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/WorkbenchWorkbenchMenu.java @@ -7,6 +7,7 @@ import com.tcm.MineTale.block.workbenches.entity.AbstractWorkbenchEntity; import com.tcm.MineTale.recipe.WorkbenchRecipeInput; import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipes; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; @@ -26,27 +27,26 @@ public class WorkbenchWorkbenchMenu extends AbstractWorkbenchContainerMenu { private final Inventory playerInventory; /** - * Creates a client-side menu instance when the workbench UI is opened. + * Creates a client-side WorkbenchWorkbenchMenu for the opened workbench UI. * - * @param syncId the synchronization id used to match this menu with the server - * @param playerInventory the player's inventory bound to this menu + * @param syncId the synchronization id that matches this menu to the server-side menu + * @param playerInventory the player's inventory to bind to this menu */ public WorkbenchWorkbenchMenu(int syncId, Inventory playerInventory) { this(syncId, playerInventory, new SimpleContainerData(EMPTY_SIZE), null); } /** - * Creates a workbench menu associated with the given player inventory and optional block entity. + * Creates a workbench menu bound to a player's inventory and an optional block entity. * - * Uses an empty internal container (size 0) and the class's data size for syncing numeric state. + * Uses an empty internal container and the class's data size for numeric state synchronization. * - * @param syncId synchronization id for this menu - * @param playerInventory the player's inventory used for slot access and recipe-book integration - * @param data container data used to sync numeric state between server and client - * @param blockEntity nullable block entity this menu is bound to, or {@code null} if not bound + * @param syncId synchronization id for this menu + * @param playerInventory the player's inventory used for slot access and recipe-book integration + * @param data container data used to sync numeric state between server and client + * @param blockEntity nullable block entity this menu is bound to, or {@code null} if not bound */ - public WorkbenchWorkbenchMenu(int syncId, Inventory playerInventory, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { - // Note: The order of arguments depends on your AbstractWorkbenchContainerMenu, + public WorkbenchWorkbenchMenu(int syncId, Inventory playerInventory, ContainerData data, @Nullable AbstractWorkbenchEntity blockEntity) { // Note: The order of arguments depends on your AbstractWorkbenchContainerMenu, // but the 'expectedSize' parameter MUST be 0. super( ModMenuTypes.WORKBENCH_WORKBENCH_MENU, @@ -56,7 +56,8 @@ public WorkbenchWorkbenchMenu(int syncId, Inventory playerInventory, ContainerDa DATA_SIZE, playerInventory, EMPTY_SIZE, - EMPTY_SIZE + EMPTY_SIZE, + ModRecipes.WORKBENCH_TYPE ); this.blockEntity = blockEntity; this.playerInventory = playerInventory; @@ -73,27 +74,13 @@ public WorkbenchWorkbenchMenu(int syncId, Inventory playerInventory, ContainerDa } /** - * Populate the given StackedItemContents with the items available through this menu for recipe-book calculations. + * Populate the given StackedItemContents with all item stacks available to the crafting UI. + * + * Accounts for items in the player's inventory, items in this menu's internal container, + * and nearby item stacks synchronised from the server so the recipe book can consider them. * - * @param stackedItemContents container to receive consolidated item counts from the menu's inventories + * @param contents the StackedItemContents to populate with available item stacks */ - // @Override - // public void fillCraftSlotsStackedContents(StackedItemContents stackedItemContents) { - // // 1. Tell the book the player has items in their pockets - // this.playerInventory.fillStackedContents(stackedItemContents); - - // // 2. Tell the book the "Nearby Chests" items also count - // AbstractWorkbenchEntity be = this.getBlockEntity(); - // if (be != null && be.isCanPullFromNearby()) { - // // This runs on the CLIENT UI, making the icons turn WHITE - // for (Container nearby : be.getNearbyInventories()) { - // for (int i = 0; i < nearby.getContainerSize(); i++) { - // stackedItemContents.accountStack(nearby.getItem(i)); - // } - // } - // } - // } - @Override public void fillCraftSlotsStackedContents(StackedItemContents contents) { // 1. Account for items in the player's pockets diff --git a/src/main/java/com/tcm/MineTale/registry/ModRecipes.java b/src/main/java/com/tcm/MineTale/registry/ModRecipes.java index 842ca60..64a44e2 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModRecipes.java +++ b/src/main/java/com/tcm/MineTale/registry/ModRecipes.java @@ -17,6 +17,8 @@ public class ModRecipes { public static final RecipeType WORKBENCH_TYPE = createType("workbench_recipe_type"); public static final RecipeType ARMORERS_TYPE = createType("armorers_recipe_type"); public static final RecipeType FURNACE_T1_TYPE = createType("furnace_t1_recipe_type"); + public static final RecipeType FARMERS_TYPE = createType("farmers_recipe_type"); + public static final RecipeType BUILDERS_TYPE = createType("builders_recipe_type"); // 2. Define the Serializers (The "How") // We pass the specific Type into the Serializer's constructor @@ -29,14 +31,20 @@ public class ModRecipes { public static final RecipeSerializer WORKBENCH_SERIALIZER = new WorkbenchRecipe.Serializer(WORKBENCH_TYPE); - public static final RecipeSerializer ARMORERS_SERIALIZER = + public static final RecipeSerializer ARMORERS_SERIALIZER = new WorkbenchRecipe.Serializer(ARMORERS_TYPE); + public static final RecipeSerializer FARMERS_SERIALIZER = + new WorkbenchRecipe.Serializer(FARMERS_TYPE); + + public static final RecipeSerializer BUILDERS_SERIALIZER = + new WorkbenchRecipe.Serializer(BUILDERS_TYPE); + /** - * Registers the mod's recipe types and their serializers into the game's built-in registries. + * Register the mod's recipe types and their serializers in Minecraft's built-in registries under the mod namespace. * - * Specifically registers the furnace (FURNACE_T1_TYPE), campfire (CAMPFIRE_TYPE), - * and workbench (WORKBENCH_TYPE) recipe types with their corresponding serializers. + * Registers FURNACE_T1_TYPE, CAMPFIRE_TYPE, WORKBENCH_TYPE, ARMORERS_TYPE, FARMERS_TYPE and BUILDERS_TYPE with their + * corresponding serializers. */ public static void initialize() { // Register the Furnace-flavored version @@ -48,6 +56,10 @@ public static void initialize() { register(WORKBENCH_TYPE, WORKBENCH_SERIALIZER); register(ARMORERS_TYPE, ARMORERS_SERIALIZER); + + register(FARMERS_TYPE, FARMERS_SERIALIZER); + + register(BUILDERS_TYPE, BUILDERS_SERIALIZER); } /**