Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/client/java/com/tcm/MineTale/MineTaleClient.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
package com.tcm.MineTale;

import com.tcm.MineTale.block.workbenches.screen.FurnaceWorkbenchScreen;
import com.tcm.MineTale.block.workbenches.screen.WorkbenchWorkbenchScreen;
import com.tcm.MineTale.block.workbenches.screen.CampfireWorkbenchScreen;
import com.tcm.MineTale.registry.ModMenuTypes;

import net.fabricmc.api.ClientModInitializer;
import net.minecraft.client.gui.screens.MenuScreens;

public class MineTaleClient implements ClientModInitializer {



/**
* Registers client-side screen factories for custom workbench menu types.
* Register client-side screen factories for custom workbench menu types.
*
* Binds the furnace and campfire workbench menu types to their corresponding screen constructors
* so the client can create the appropriate GUI when those menus are opened.
* Binds ModMenuTypes.FURNACE_WORKBENCH_MENU to FurnaceWorkbenchScreen,
* ModMenuTypes.CAMPFIRE_WORKBENCH_MENU to CampfireWorkbenchScreen, and
* ModMenuTypes.WORKBENCH_WORKBENCH_MENU to WorkbenchWorkbenchScreen so the client
* can create the appropriate GUI when those menus open.
*/
@Override
public void onInitializeClient() {
MenuScreens.register(ModMenuTypes.FURNACE_WORKBENCH_MENU, FurnaceWorkbenchScreen::new);
MenuScreens.register(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, CampfireWorkbenchScreen::new);
MenuScreens.register(ModMenuTypes.WORKBENCH_WORKBENCH_MENU, WorkbenchWorkbenchScreen::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
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.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;
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.gui.screens.recipebook.RecipeCollection;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
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 WorkbenchWorkbenchScreen extends AbstractRecipeBookScreen<WorkbenchWorkbenchMenu> {
private static final Identifier TEXTURE =
Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/workbench_workbench.png");

private final MineTaleRecipeBookComponent mineTaleRecipeBook;

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 WorkbenchWorkbenchScreen(WorkbenchWorkbenchMenu menu, Inventory inventory, Component title) {
this(menu, inventory, title, createRecipeBookComponent(menu));
}

/**
* Creates a WorkbenchWorkbenchScreen 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 WorkbenchWorkbenchScreen(WorkbenchWorkbenchMenu 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(WorkbenchWorkbenchMenu menu) {
ItemStack tabIcon = new ItemStack(ModBlocks.WORKBENCH_WORKBENCH_BLOCK.asItem());

List<RecipeBookComponent.TabInfo> tabs = List.of(
new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.WORKBENCH_SEARCH)
);

return new MineTaleRecipeBookComponent(menu, tabs, ModRecipes.WORKBENCH_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", "30", "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(30);
}).bounds(defaultLeft, defaultTop + 22, 35, 20).build());
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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) {
// 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<ItemStack> 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!");
}
}

/**
* 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);

// Get the ID of the recipe clicked in the ghost-book
RecipeDisplayId displayId = this.mineTaleRecipeBook.getSelectedRecipeId();
RecipeDisplayEntry selectedEntry = null;

if (displayId != null && this.minecraft.level != null) {
ClientRecipeBook book = this.minecraft.player.getRecipeBook();
// Accessing the known recipes via your Accessor
Map<RecipeDisplayId, RecipeDisplayEntry> knownRecipes = ((ClientRecipeBookAccessor) book).getKnown();
selectedEntry = knownRecipes.get(displayId);
}

// 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 canCraftTen = canCraft(this.minecraft.player, selectedEntry, 10);

this.craftOneBtn.active = canCraftOne;
this.craftTenBtn.active = canCraftTen;
this.craftAllBtn.active = canCraftOne;
} else {
this.craftOneBtn.active = false;
this.craftTenBtn.active = false;
this.craftAllBtn.active = false;
}

renderTooltip(graphics, mouseX, mouseY);
}

private boolean canCraft(Player player, RecipeDisplayEntry entry, int craftCount) {
if (player == null || entry == null) return false;

// craftingRequirements() provides the list of all items (the 15 items for your chest)
Optional<List<Ingredient>> reqs = entry.craftingRequirements();
if (reqs.isEmpty()) return false;

// 1. Group duplicate ingredients (e.g., 5 Log entries become 1 Log entry with a value of 5)
Map<Ingredient, Integer> aggregatedRequirements = new HashMap<>();
for (Ingredient ing : reqs.get()) {
aggregatedRequirements.put(ing, aggregatedRequirements.getOrDefault(ing, 0) + 1);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find where Ingredient is defined
find . -name "*.java" -type f | xargs grep -l "class Ingredient\|record Ingredient" | head -20

Repository: CodeMonkeysMods/MineTale

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Search for Ingredient class/record definition more broadly
rg -n "^\s*(public\s+)?(final\s+)?(class|record)\s+Ingredient\s*[\{<]" --type=java

Repository: CodeMonkeysMods/MineTale

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Also check in the current file to see what Ingredient is being used
cat -n src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java | head -50

Repository: CodeMonkeysMods/MineTale

Length of output: 2717


🏁 Script executed:

#!/bin/bash
# Get context around lines 214-217
sed -n '200,230p' src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java

Repository: CodeMonkeysMods/MineTale

Length of output: 1444


🏁 Script executed:

#!/bin/bash
# Search for other uses of Ingredient in the codebase to understand how it's used
rg "Ingredient" src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java -B 2 -A 2

Repository: CodeMonkeysMods/MineTale

Length of output: 2028


🌐 Web query:

Minecraft Ingredient class equals hashCode implementation net.minecraft.world.item.crafting

💡 Result:

net.minecraft.world.item.crafting.Ingredient does not override equals() or hashCode()—both are inherited directly from java.lang.Object (i.e., identity/reference equality and the default identity-based hash code). So two Ingredient instances that match the same items are not considered equal unless they are literally the same object instance. [1]

Same behavior applies to older mappings/packages like net.minecraft.item.crafting.Ingredient (e.g., 1.16.5-era): equals()/hashCode() are also inherited from Object. [2]

Sources:
[1] (nekoyue.github.io)
[2] (nekoyue.github.io)

Citations:


Using Ingredient as a HashMap key will not aggregate correctly.

net.minecraft.world.item.crafting.Ingredient does not override equals() or hashCode(), so it uses identity-based equality. Two different Ingredient objects representing the same item will be treated as distinct keys, preventing the intended aggregation. For example, if the list contains five separate Ingredient instances for logs, the HashMap will create five entries with count=1 each instead of one entry with count=5.

Change to aggregate ingredients by comparing their matched items or use a different data structure. For example, you could extract item stacks from each ingredient and use those as keys, or switch to a List<Pair<Ingredient, Integer>> where you manually check for matches.

🤖 Prompt for AI Agents
In
`@src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java`
around lines 214 - 217, The aggregation using Map<Ingredient,Integer>
(aggregatedRequirements) fails because Ingredient uses identity equality; change
aggregation to use a stable key derived from the Ingredient (for example the
matching Item or ItemStack) or perform manual matching: iterate reqs.get(),
obtain a representative key (e.g., ing.getItems()[0].getItem() or an ItemStack
identity) and use that key in the map instead of Ingredient, or switch to a
List<Pair<Ingredient,Integer>> and when adding each Ingredient use
Ingredient.test(existingStack) or Ingredient#isSame to find an existing entry
and increment its count; update all references to aggregatedRequirements and any
downstream code to expect the chosen key type.


// 2. Check the player's inventory against the totals
Inventory inv = player.getInventory();
for (Map.Entry<Ingredient, Integer> entryReq : aggregatedRequirements.entrySet()) {
// totalNeeded = (Amount in 1 recipe) * (Number of crafts, e.g. 1 or 30)
int totalNeeded = entryReq.getValue() * craftCount;

if (!hasIngredientAmount(inv, entryReq.getKey(), totalNeeded)) {
return false; // Player doesn't have enough of this specific ingredient
}
}

return true;
}
Comment on lines +214 to +252
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the WorkbenchWorkbenchScreen.java file and check if canCraft is called in render()
find . -name "WorkbenchWorkbenchScreen.java" -type f

Repository: CodeMonkeysMods/MineTale

Length of output: 157


🏁 Script executed:

# Search for the render method and canCraft invocations in the file
rg -n "render\(|canCraft" --type=java -A 5 -B 2 | head -100

Repository: CodeMonkeysMods/MineTale

Length of output: 12497


🏁 Script executed:

# Search for HolderSet class definition and its equals/hashCode implementation
rg -n "class HolderSet|equals|hashCode" --type=java -A 3 -B 1 | grep -A 5 "HolderSet" | head -150

Repository: CodeMonkeysMods/MineTale

Length of output: 839


🏁 Script executed:

# Look for HolderSet.direct method
rg -n "direct\(" --type=java -B 3 -A 10 | grep -A 10 "HolderSet" | head -100

Repository: CodeMonkeysMods/MineTale

Length of output: 50


🏁 Script executed:

# Find imports of HolderSet in the WorkbenchWorkbenchScreen file
rg -n "import.*HolderSet" src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java

Repository: CodeMonkeysMods/MineTale

Length of output: 107


🏁 Script executed:

# Search for HolderSet class definition in entire codebase
rg -n "class HolderSet" --type=java

Repository: CodeMonkeysMods/MineTale

Length of output: 50


🏁 Script executed:

# Look for HolderSet.direct method definition
rg -n "static.*direct\(" --type=java -A 10 | grep -B 2 -A 10 "HolderSet"

Repository: CodeMonkeysMods/MineTale

Length of output: 50


🏁 Script executed:

# Check if HolderSet equals/hashCode are defined anywhere in the codebase
fd -t f -name "*.java" -exec grep -l "HolderSet" {} \; | head -20

Repository: CodeMonkeysMods/MineTale

Length of output: 298


🏁 Script executed:

# Find all Java files that reference HolderSet to understand its usage pattern
rg -l "HolderSet" --type=java | head -10

Repository: CodeMonkeysMods/MineTale

Length of output: 155


🏁 Script executed:

# Check the actual usage of HolderSet.direct in the codebase
rg -n "HolderSet\.direct" --type=java -B 3 -A 3

Repository: CodeMonkeysMods/MineTale

Length of output: 50


🌐 Web query:

Minecraft HolderSet.direct equals hashCode implementation net.minecraft.core

💡 Result:

In older Minecraft versions (e.g., 1.18.2), net.minecraft.core.HolderSet.Direct does not override equals() or hashCode() (so it uses Object identity semantics). This is visible in the 1.18.2 docs: the method list for HolderSet.Direct doesn’t include equals/hashCode, and they appear only under “Methods inherited from class java.lang.Object”. [1]

In newer versions (at least NeoForge’s 1.21.10-based sources), net.minecraft.core.HolderSet.Direct does override both equals(Object) and hashCode(). [2]

What those overrides are intended to mean (per the class shape shown in mappings/docs) is “value equality” based on its stored holders—i.e., the contents (List<Holder<T>>) is the backing state, with an internal cached contentsSet for fast membership checks. [2][3]

Sources:
[1] Forge JavaDocs (MC 1.18.2): HolderSet.Direct shows no declared equals/hashCode (only inherited).
[2] NeoForge JavaDocs (MC 1.21.10): HolderSet.Direct declares equals(Object) and hashCode().
[3] Mappings.dev (1.21.8): shows HolderSet$Direct fields contents and contentsSet.


🌐 Web query:

HolderSet Minecraft 1.20 equals method stable hash

💡 Result:

In Minecraft 1.20.x, net.minecraft.core.HolderSet implementations like HolderSet.Direct and HolderSet.Named do not override equals(Object) or hashCode()—their method listings contain no equals/hashCode methods. (mappings.dev)

So:

  • equals is identity-based (same as Object.equals): two different HolderSet instances with the same entries/tag are not equal.
  • hashCode is identity-based (same as Object.hashCode): it’s stable for the lifetime of that instance, but not content-based and not stable across different instances/runs.

If you need content-based equality/stable hashing, you must compute it yourself (e.g., from unwrap() / tag key + entries) rather than relying on HolderSet’s equals/hashCode. (mappings.dev)

Citations:


🏁 Script executed:

# Check build configuration for Minecraft version
find . -name "gradle.properties" -o -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" | head -5

Repository: CodeMonkeysMods/MineTale

Length of output: 102


🏁 Script executed:

# Search for minecraft version strings in config files
rg -n "minecraft" gradle.properties build.gradle build.gradle.kts 2>/dev/null | head -20

Repository: CodeMonkeysMods/MineTale

Length of output: 193


canCraft runs every frame inside render() — cache the results instead of recomputing.

The ingredient aggregation logic (stream → collect → HolderSet.direct → HashMap lookup per ingredient) runs three times per render tick at lines 198–200 to update button states. This is unnecessary: cache the results and update only when the player's inventory changes (via a change listener) or when the selected recipe changes, not every frame.

While HolderSet.direct(...) properly implements value-based equals/hashCode in Minecraft 1.21.11, the repeated stream/collection overhead on every render is wasteful.

🤖 Prompt for AI Agents
In
`@src/client/java/com/tcm/MineTale/block/workbenches/screen/WorkbenchWorkbenchScreen.java`
around lines 214 - 252, canCraft currently rebuilds aggregatedRequirements and
holderToIngredient every frame (called from render()); move that aggregation out
of canCraft into cached fields (e.g., cachedAggregatedRequirements and
cachedHolderToIngredient) keyed by the currently selected RecipeDisplayEntry and
update the cache only when the selected recipe changes or the player's inventory
changes. Modify the screen to detect recipe changes (track lastSelectedRecipe)
and listen for inventory changes or track an inventory version/timestamp on
Player (or add an inventory change listener) and recompute the maps when those
change; keep canCraft using the cachedAggregatedRequirements and
cachedHolderToIngredient and only call hasIngredientAmount for totals (use
craftCount multiplication) so per-frame work is limited to lookups and not
stream/collect/HolderSet.direct operations.


private boolean hasIngredientAmount(Inventory inventory, Ingredient ingredient, int totalRequired) {
System.out.println("DEBUG: Searching inventory for " + totalRequired + " of an ingredient...");
if (totalRequired <= 0) return true;

int found = 0;
for (int i = 0; i < inventory.getContainerSize(); i++) {
ItemStack stack = inventory.getItem(i);
if (!stack.isEmpty() && ingredient.test(stack)) {
found += stack.getCount();
System.out.println("DEBUG: Found " + stack.getCount() + " in slot " + i + ". Total found: " + found);
}
if (found >= totalRequired) {
System.out.println("DEBUG: Ingredient requirement MET");
return true;
}
}

System.out.println("DEBUG: Ingredient requirement 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);
}
}
59 changes: 54 additions & 5 deletions src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,31 @@
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.minecraft.client.data.models.BlockModelGenerators;
import net.minecraft.client.data.models.ItemModelGenerators;
import net.minecraft.client.data.models.blockstates.MultiVariantGenerator;
import net.minecraft.client.data.models.blockstates.PropertyDispatch;
import net.minecraft.client.data.models.model.ModelLocationUtils;
import net.minecraft.client.renderer.block.model.VariantMutator;
import net.minecraft.core.Direction;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.ChestType;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;

public class ModModelProvider extends FabricModelProvider {
/**
* Creates a ModModelProvider using the given Fabric data output.
*
* @param output the FabricDataOutput used to write generated model and blockstate data
*/
public ModModelProvider(FabricDataOutput output) { super(output); }
* Creates a ModModelProvider using the given Fabric data output.
*
* @param output the FabricDataOutput used to write generated model and blockstate data
*/
public ModModelProvider(FabricDataOutput output) { super(output); }

// Recreate the rotation logic locally since the base one is private
private static final PropertyDispatch<VariantMutator> WORKBENCH_ROTATION = PropertyDispatch.modify(BlockStateProperties.HORIZONTAL_FACING)
.select(Direction.NORTH, BlockModelGenerators.NOP)
.select(Direction.EAST, BlockModelGenerators.Y_ROT_90)
.select(Direction.SOUTH, BlockModelGenerators.Y_ROT_180)
.select(Direction.WEST, BlockModelGenerators.Y_ROT_270);

/**
* Registers block state and model definitions for the mod's log blocks.
Expand Down Expand Up @@ -54,6 +71,38 @@ public void generateBlockStateModels(BlockModelGenerators blockStateModelGenerat
blockStateModelGenerator.woodProvider(ModBlocks.STRIPPED_LOG).logWithHorizontal(ModBlocks.STRIPPED_LOG);
blockStateModelGenerator.woodProvider(ModBlocks.WINDWILLOW_LOG).logWithHorizontal(ModBlocks.WINDWILLOW_LOG);
blockStateModelGenerator.woodProvider(ModBlocks.WILD_WISTERIA_LOG).logWithHorizontal(ModBlocks.WILD_WISTERIA_LOG).wood(ModBlocks.WILD_WISTERIA_WOOD);

registerLargeWorkbench(blockStateModelGenerator, ModBlocks.FURNACE_WORKBENCH_BLOCK_T1);
}
Comment on lines +74 to +77
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if WORKBENCH_WORKBENCH_BLOCK exists and whether it has any model JSON or other datagen registration
echo "=== Searching for WORKBENCH_WORKBENCH_BLOCK definition ==="
rg -n "WORKBENCH_WORKBENCH_BLOCK" --type=java -C 3

echo ""
echo "=== Searching for hand-written workbench model JSONs ==="
find . -path "*/models/block/*workbench*" -type f | sort

echo ""
echo "=== Searching for hand-written blockstate JSONs ==="
find . -path "*/blockstates/*workbench*" -type f | sort

Repository: CodeMonkeysMods/MineTale

Length of output: 5360


🏁 Script executed:

#!/bin/bash
echo "=== Checking WorkbenchWorkbench class structure ==="
rg -n "class WorkbenchWorkbench" --type=java -A 20

echo ""
echo "=== Searching for all methods in ModModelProvider.java ==="
rg -n "def |void |public " src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java | head -30

echo ""
echo "=== Check if WORKBENCH_WORKBENCH_BLOCK is registered anywhere in ModModelProvider ==="
rg -n "WORKBENCH_WORKBENCH_BLOCK" src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java

Repository: CodeMonkeysMods/MineTale

Length of output: 11567


🏁 Script executed:

#!/bin/bash
echo "=== Full generateBlockStateModels method ==="
sed -n '43,77p' src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java

Repository: CodeMonkeysMods/MineTale

Length of output: 3657


Add model registration for WORKBENCH_WORKBENCH_BLOCK in generateBlockStateModels.

The new WORKBENCH_WORKBENCH_BLOCK is registered in ModBlocks.java and added to creative tabs but is missing model/blockstate generation in ModModelProvider.java. Since WorkbenchWorkbench is a 1×1 block (not a 2×2 multiblock like the furnace workbenches), it needs its own model registration—otherwise it will render as a missing-model purple-and-black cube in-game. Either generate models via datagen or provide hand-written blockstate/model JSON files.

🤖 Prompt for AI Agents
In `@src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java` around lines
74 - 76, generateBlockStateModels is missing model registration for the new 1x1
WORKBENCH_WORKBENCH_BLOCK, so it renders as a missing-model in-game; fix it by
adding a registration call in ModModelProvider.generateBlockStateModels (next to
registerLargeWorkbench(...)) to generate a standard model for
ModBlocks.WORKBENCH_WORKBENCH_BLOCK — e.g., call the same simple-block helper
used elsewhere (registerSimpleBlock or the project’s equivalent) with
blockStateModelGenerator and ModBlocks.WORKBENCH_WORKBENCH_BLOCK, or if you need
a custom model use the generator’s model creation helper to produce the correct
blockstate/model JSON.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workbenchworkbench is a 1x2 model so needs to be two wide one tall.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Check WorkbenchWorkbench class for properties ==="
cat src/main/java/com/tcm/MineTale/block/workbenches/WorkbenchWorkbench.java

echo ""
echo "=== Search for workbench_workbench model JSON files ==="
find . -path "*/models/block/*workbench_workbench*" -type f 2>/dev/null | sort

echo ""
echo "=== Check if workbench_workbench blockstate JSON exists ==="
find . -path "*/blockstates/*workbench_workbench*" -type f 2>/dev/null

echo ""
echo "=== Check AbstractWorkbench for default properties ==="
rg -n "class AbstractWorkbench" --type=java -A 30 | head -50

Length of output: 11513


🏁 Script executed:

#!/bin/bash
echo "=== Check AbstractWorkbench class and constructor ==="
rg -n "class AbstractWorkbench" --type=java -A 60 | head -80

echo ""
echo "=== Search how IS_WIDE and IS_TALL are used in AbstractWorkbench ==="
rg -n "isWide|isTall" src/main/java/com/tcm/MineTale/block/workbenches/ --type=java -C 2

Length of output: 17720


@The-Code-Monkey, thank you for the clarification! I see the issue now—there's a mismatch between your design intent and the current code.

Looking at WorkbenchWorkbench.java lines 26-27, the constants are currently set as:

public static final boolean IS_WIDE = false;
public static final boolean IS_TALL = false;

This configures it as a 1×1 block (single block). For a 1×2 block (two wide, one tall), you need:

public static final boolean IS_WIDE = true;  // ← Change to true
public static final boolean IS_TALL = false;

When IS_WIDE = true, AbstractWorkbench will:

  • Place two blocks side-by-side (LEFT and RIGHT via ChestType)
  • Set the initial block as ChestType.LEFT and place a companion ChestType.RIGHT block (see AbstractWorkbench.java lines 84-87)

Two fixes are needed:

  1. Fix the constants in WorkbenchWorkbench.java: Change IS_WIDE from false to true

  2. Add model registration in ModModelProvider.java: Since a 1×2 workbench uses only ChestType (LEFT/RIGHT) without DoubleBlockHalf, you'll need either:

    • A new registration method for 1×2 blocks, or
    • Adapt registerLargeWorkbench to handle 1×2 variants

    Plus, create the corresponding model JSON files:

    • workbench_workbench_left.json
    • workbench_workbench_right.json
    • workbench_workbench_inventory.json

Would you like me to help generate the model registration code for the 1×2 workbench?


🧬 Code Graph Analysis Results

src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java

  1. Snippet:WORKBENCH_ROTATION constant
  • File: src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java
  • Line range (approximate): 16-24
  • Content:
private static final PropertyDispatch<VariantMutator> WORKBENCH_ROTATION = PropertyDispatch.modify(BlockStateProperties.HORIZONTAL_FACING)
        .select(Direction.NORTH, BlockModelGenerators.NOP)
        .select(Direction.EAST, BlockModelGenerators.Y_ROT_90)
        .select(Direction.SOUTH, BlockModelGenerators.Y_ROT_180)
        .select(Direction.WEST, BlockModelGenerators.Y_ROT_270);
  1. Snippet: registerLargeWorkbench method
  • File: src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java
  • Line range (approximate): 38-74 (method plus header comments)
  • Content:
private void registerLargeWorkbench(BlockModelGenerators generator, Block block) {
    // 1. Get the base identifier (e.g., minetale:block/furnace_workbench_block_t1)
    Identifier blockId = ModelLocationUtils.getModelLocation(block);

    // 2. Build the references to your manual JSON files
    // .withSuffix() creates: minetale:block/furnace_workbench_block_t1_bottom_left, etc.
    Identifier bottomLeft  = blockId.withSuffix("_bottom_left");
    Identifier bottomRight = blockId.withSuffix("_bottom_right");
    Identifier topLeft     = blockId.withSuffix("_top_left");
    Identifier topRight    = blockId.withSuffix("_top_right");
    Identifier inventory   = blockId.withSuffix("_inventory");

    // 3. Dispatch to Blockstate (Tells the game which model to show for each state)
    generator.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
        .with(PropertyDispatch.initial(BlockStateProperties.DOUBLE_BLOCK_HALF, BlockStateProperties.CHEST_TYPE)
            .select(DoubleBlockHalf.LOWER, ChestType.LEFT,   BlockModelGenerators.plainVariant(bottomLeft))
            .select(DoubleBlockHalf.LOWER, ChestType.RIGHT,  BlockModelGenerators.plainVariant(bottomRight))
            .select(DoubleBlockHalf.UPPER, ChestType.LEFT,   BlockModelGenerators.plainVariant(topLeft))
            .select(DoubleBlockHalf.UPPER, ChestType.RIGHT,  BlockModelGenerators.plainVariant(topRight))
            // Support the 'SINGLE' state as a fallback
            .select(DoubleBlockHalf.LOWER, ChestType.SINGLE, BlockModelGenerators.plainVariant(bottomLeft))
            .select(DoubleBlockHalf.UPPER, ChestType.SINGLE, BlockModelGenerators.plainVariant(topLeft))
        )
        .with(WORKBENCH_ROTATION)
    );

    // 4. Map the Item in your hand to the inventory JSON
    generator.registerSimpleItemModel(block, inventory);
}

✏️ Learnings added
Learnt from: The-Code-Monkey
Repo: CodeMonkeysMods/MineTale PR: 30
File: src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java:74-76
Timestamp: 2026-02-13T08:23:39.499Z
Learning: The WORKBENCH_WORKBENCH_BLOCK in WorkbenchWorkbench.java should be configured as a 1x2 block (two wide, one tall) by setting IS_WIDE = true and IS_TALL = false, not as a 1x1 block.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +74 to +77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

FURNACE_WORKBENCH_BLOCK_T2 is also missing model registration.

WORKBENCH_WORKBENCH_BLOCK was already flagged, but FURNACE_WORKBENCH_BLOCK_T2 is also registered in ModBlocks yet has no corresponding registerLargeWorkbench call here. It will render as a purple-black missing-model cube in-game.

Proposed fix
         registerLargeWorkbench(blockStateModelGenerator, ModBlocks.FURNACE_WORKBENCH_BLOCK_T1);
+        registerLargeWorkbench(blockStateModelGenerator, ModBlocks.FURNACE_WORKBENCH_BLOCK_T2);
     }

This will also require the corresponding T2 model JSON files (furnace_workbench_block_t2_bottom_left.json, etc.) to exist in the resources.

🤖 Prompt for AI Agents
In `@src/client/java/com/tcm/MineTale/datagen/ModModelProvider.java` around lines
74 - 76, ModModelProvider is missing a call to register the T2 workbench model:
add a registerLargeWorkbench(blockStateModelGenerator,
ModBlocks.FURNACE_WORKBENCH_BLOCK_T2) invocation (in the same area where
registerLargeWorkbench is used for T1 and WORKBENCH_WORKBENCH_BLOCK) so the
block gets proper models, and ensure the corresponding T2 model JSONs
(furnace_workbench_block_t2_bottom_left.json, etc.) are added to resources;
locate the registerLargeWorkbench usage in ModModelProvider to insert the call.


private void registerLargeWorkbench(BlockModelGenerators generator, Block block) {
// 1. Get the base identifier (e.g., minetale:block/furnace_workbench_block_t1)
Identifier blockId = ModelLocationUtils.getModelLocation(block);

// 2. Build the references to your manual JSON files
// .withSuffix() creates: minetale:block/furnace_workbench_block_t1_bottom_left, etc.
Identifier bottomLeft = blockId.withSuffix("_bottom_left");
Identifier bottomRight = blockId.withSuffix("_bottom_right");
Identifier topLeft = blockId.withSuffix("_top_left");
Identifier topRight = blockId.withSuffix("_top_right");
Identifier inventory = blockId.withSuffix("_inventory");

// 3. Dispatch to Blockstate (Tells the game which model to show for each state)
generator.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
.with(PropertyDispatch.initial(BlockStateProperties.DOUBLE_BLOCK_HALF, BlockStateProperties.CHEST_TYPE)
.select(DoubleBlockHalf.LOWER, ChestType.LEFT, BlockModelGenerators.plainVariant(bottomLeft))
.select(DoubleBlockHalf.LOWER, ChestType.RIGHT, BlockModelGenerators.plainVariant(bottomRight))
.select(DoubleBlockHalf.UPPER, ChestType.LEFT, BlockModelGenerators.plainVariant(topLeft))
.select(DoubleBlockHalf.UPPER, ChestType.RIGHT, BlockModelGenerators.plainVariant(topRight))
// Support the 'SINGLE' state as a fallback
.select(DoubleBlockHalf.LOWER, ChestType.SINGLE, BlockModelGenerators.plainVariant(bottomLeft))
.select(DoubleBlockHalf.UPPER, ChestType.SINGLE, BlockModelGenerators.plainVariant(topLeft))
)
.with(WORKBENCH_ROTATION)
);

// 4. Map the Item in your hand to the inventory JSON
generator.registerSimpleItemModel(block, inventory);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
Expand Down
Loading