-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/add builders workbench #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
The-Code-Monkey
merged 1 commit into
feat/add-farmers-workbench
from
feat/add-builders-workbench
Feb 26, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
300 changes: 300 additions & 0 deletions
300
src/client/java/com/tcm/MineTale/block/workbenches/screen/BuildersWorkbenchScreen.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,300 @@ | ||
| 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<BuildersWorkbenchMenu> { | ||
| 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<RecipeBookComponent.TabInfo> tabs = List.of( | ||
| new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.BUILDERS_SEARCH) | ||
| ); | ||
|
|
||
| return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.BUILDERS_TYPE); | ||
| } | ||
|
|
||
| /** | ||
| * 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. | ||
| */ | ||
| @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()); | ||
| } | ||
|
|
||
| /** | ||
| * Sends a crafting request for the currently selected recipe in the integrated recipe book. | ||
| * | ||
| * 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. | ||
| * | ||
| * @param amount the quantity to craft; use -1 to request crafting of the full available stack ("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<ItemStack> 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!"); | ||
| } | ||
|
|
||
| /** | ||
| * Draws the workbench GUI background texture at the screen's top-left corner. | ||
| * | ||
| * @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 | ||
| */ | ||
| 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); | ||
| } | ||
|
|
||
| @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); | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether the player has enough ingredients to craft the given recipe the specified 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 (e.g., 1, 10, or -1 is not specially handled here) | ||
| * @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<List<Ingredient>> reqs = entry.craftingRequirements(); | ||
| if (reqs.isEmpty()) return false; | ||
|
|
||
| // 1. Group ingredients by their underlying Item Holders. | ||
| // Using List<Holder<Item>> as the key ensures structural equality (content-based hashing). | ||
| Map<List<Holder<Item>>, Integer> aggregatedRequirements = new HashMap<>(); | ||
| Map<List<Holder<Item>>, Ingredient> holderToIngredient = new HashMap<>(); | ||
|
|
||
| for (Ingredient ing : reqs.get()) { | ||
| // Collect holders into a List to get a stable hashCode() and equals() | ||
| @SuppressWarnings("deprecation") | ||
| List<Holder<Item>> 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<List<Holder<Item>>, Integer> entryReq : aggregatedRequirements.entrySet()) { | ||
| List<Holder<Item>> 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; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| /** | ||
| * Computes the on-screen position for the recipe book toggle button for this GUI. | ||
| * | ||
| * @return the screen position placed 5 pixels from the GUI's left edge and 49 pixels above the GUI's vertical center | ||
| */ | ||
| @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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 20 additions & 16 deletions
36
src/client/java/com/tcm/MineTale/datagen/recipes/BuilderRecipes.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,32 @@ | ||
| 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 { | ||
| 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"); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.