diff --git a/src/client/java/com/tcm/MineTale/MineTaleClient.java b/src/client/java/com/tcm/MineTale/MineTaleClient.java index 08f7ef7..836fb08 100644 --- a/src/client/java/com/tcm/MineTale/MineTaleClient.java +++ b/src/client/java/com/tcm/MineTale/MineTaleClient.java @@ -8,6 +8,9 @@ import net.minecraft.client.gui.screens.MenuScreens; public class MineTaleClient implements ClientModInitializer { + + + /** * Registers client-side screen factories for custom workbench menu types. * diff --git a/src/client/java/com/tcm/MineTale/block/workbenches/screen/CampfireWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/CampfireWorkbenchScreen.java index 3ee2b7f..e0ec094 100644 --- a/src/client/java/com/tcm/MineTale/block/workbenches/screen/CampfireWorkbenchScreen.java +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/CampfireWorkbenchScreen.java @@ -1,18 +1,27 @@ package com.tcm.MineTale.block.workbenches.screen; +import java.util.List; + import com.tcm.MineTale.MineTale; import com.tcm.MineTale.block.workbenches.menu.CampfireWorkbenchMenu; +import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; +import com.tcm.MineTale.registry.ModBlocks; +import com.tcm.MineTale.registry.ModRecipeDisplay; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +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.resources.Identifier; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; import net.minecraft.network.chat.Component; -public class CampfireWorkbenchScreen extends AbstractContainerScreen { +public class CampfireWorkbenchScreen extends AbstractRecipeBookScreen { private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/furnace_workbench.png"); + /** * Creates a campfire workbench screen for the provided menu, player inventory, and title. * @@ -21,7 +30,21 @@ public class CampfireWorkbenchScreen extends AbstractContainerScreen tabs = List.of( + new RecipeBookComponent.TabInfo(tabIcon.getItem(), ModRecipeDisplay.CAMPFIRE_SEARCH) + ); + + return new MineTaleRecipeBookComponent(menu, tabs); } /** @@ -29,8 +52,11 @@ public CampfireWorkbenchScreen(CampfireWorkbenchMenu menu, Inventory inventory, */ @Override protected void init() { + // Important: Set your GUI size before super.init() + this.imageWidth = 176; + this.imageHeight = 166; + super.init(); - this.titleLabelX = (this.imageWidth - this.font.width(this.title)) / 2; } /** @@ -57,8 +83,25 @@ protected void renderBg(GuiGraphics guiGraphics, float f, int i, int j) { */ @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + // 1. Always render the dark background tint first renderBackground(graphics, mouseX, mouseY, delta); + + // 3. Call super (this draws your slots and items) super.render(graphics, mouseX, mouseY, delta); + renderTooltip(graphics, mouseX, mouseY); } + + @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/FurnaceWorkbenchScreen.java b/src/client/java/com/tcm/MineTale/block/workbenches/screen/FurnaceWorkbenchScreen.java index f4f4ded..882ae2c 100644 --- a/src/client/java/com/tcm/MineTale/block/workbenches/screen/FurnaceWorkbenchScreen.java +++ b/src/client/java/com/tcm/MineTale/block/workbenches/screen/FurnaceWorkbenchScreen.java @@ -1,65 +1,104 @@ package com.tcm.MineTale.block.workbenches.screen; +import java.util.List; + import com.tcm.MineTale.MineTale; import com.tcm.MineTale.block.workbenches.menu.FurnaceWorkbenchMenu; +import com.tcm.MineTale.recipe.MineTaleRecipeBookComponent; +import com.tcm.MineTale.registry.ModBlocks; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +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.resources.Identifier; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeBookCategories; import net.minecraft.network.chat.Component; -public class FurnaceWorkbenchScreen extends AbstractContainerScreen { +public class FurnaceWorkbenchScreen extends AbstractRecipeBookScreen { private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "textures/gui/container/furnace_workbench.png"); /** - * Creates a new furnace workbench screen for the given menu, player inventory, and title. - * - * @param menu the container menu that provides slots and syncs 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 + * Creates a new furnace workbench screen. + * Note: recipeBookComponent is inherited from AbstractRecipeBookScreen. */ public FurnaceWorkbenchScreen(FurnaceWorkbenchMenu menu, Inventory inventory, Component title) { - super(menu, inventory, title); + super(menu, createRecipeBookComponent(menu), inventory, title); } /** - * Initializes the screen and centers the title horizontally by setting {@code titleLabelX}. + * Static helper to build the component with the custom MineTale tabs. + * This uses the FURNACE_WORKBENCH icon for this specific screen's tab. */ + private static MineTaleRecipeBookComponent createRecipeBookComponent(FurnaceWorkbenchMenu menu) { + ItemStack tabIcon = new ItemStack(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1.asItem()); + + List tabs = List.of( + new RecipeBookComponent.TabInfo(tabIcon.getItem(), RecipeBookCategories.CRAFTING_MISC) + ); + + return new MineTaleRecipeBookComponent(menu, tabs); + } + @Override protected void init() { + this.imageWidth = 176; + this.imageHeight = 166; + super.init(); - this.titleLabelX = (this.imageWidth - this.font.width(this.title)) / 2; + + // // Initialize the inherited recipeBookComponent UI state + // this.recipeBookComponent.init(this.width, this.height, this.minecraft, false); + // this.leftPos = this.recipeBookComponent.updateScreenPosition(this.width, this.imageWidth); + + // // The toggle button is managed via getRecipeBookButtonPosition() in 1.21.1 + // // but we add the ImageButton manually to match your CampfireWorkbenchScreen logic exactly. + // this.addRenderableWidget(new ImageButton( + // this.leftPos + 5, + // this.height / 2 - 49, + // 20, 18, + // RecipeBookComponent.RECIPE_BUTTON_SPRITES, + // (button) -> { + // this.recipeBookComponent.toggleVisibility(); + // this.leftPos = this.recipeBookComponent.updateScreenPosition(this.width, this.imageWidth); + // button.setPosition(this.leftPos + 5, this.height / 2 - 49); + // } + // )); } - /** - * Draws the furnace workbench background texture onto the screen. - * - * @param guiGraphics the graphics context used for drawing - * @param f partial tick time used for interpolation - * @param i current mouse x position - * @param j current mouse y position - */ - 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 + 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 furnace workbench screen, drawing its background, contents, and tooltips. - * - * @param graphics the graphics context used for rendering - * @param mouseX the current mouse X coordinate - * @param mouseY the current mouse Y coordinate - * @param delta the frame time delta (partial tick) used for animated rendering - */ @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { renderBackground(graphics, mouseX, mouseY, delta); + + // if (this.recipeBookComponent != null) { + // this.recipeBookComponent.render(graphics, mouseX, mouseY, delta); + // } + super.render(graphics, mouseX, mouseY, delta); + + // if (this.recipeBookComponent != null) { + // this.recipeBookComponent.renderGhostRecipe(graphics, true); + // this.recipeBookComponent.renderTooltip(graphics, this.leftPos, this.topPos, this.hoveredSlot); + // } + renderTooltip(graphics, mouseX, mouseY); } + + @Override + protected ScreenPosition getRecipeBookButtonPosition() { + int guiLeft = (this.width - this.imageWidth) / 2; + int guiTop = (this.height - this.imageHeight) / 2; + 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/mixin/client/ClientRecipeBookMixin.java b/src/client/java/com/tcm/MineTale/mixin/client/ClientRecipeBookMixin.java new file mode 100644 index 0000000..6e38bb1 --- /dev/null +++ b/src/client/java/com/tcm/MineTale/mixin/client/ClientRecipeBookMixin.java @@ -0,0 +1,24 @@ +package com.tcm.MineTale.mixin.client; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.tcm.MineTale.registry.ModRecipeDisplay; +import com.tcm.MineTale.registry.ModRecipes; + +import net.minecraft.client.ClientRecipeBook; +import net.minecraft.world.item.crafting.RecipeBookCategory; +import net.minecraft.world.item.crafting.RecipeHolder; + +@Mixin(ClientRecipeBook.class) +public abstract class ClientRecipeBookMixin { + @Inject(method = "getCategory", at = @At("HEAD"), cancellable = true) + private static void minetale$addCustomCategory(RecipeHolder recipe, CallbackInfoReturnable cir) { + if (recipe.value().getType() == ModRecipes.FURNACE_SERIALIZER) { + // This tells the search engine to put your recipes into your custom tab + cir.setReturnValue(ModRecipeDisplay.CAMPFIRE_SEARCH); + } + } +} \ No newline at end of file diff --git a/src/client/java/com/tcm/MineTale/recipe/MineTaleRecipeBookComponent.java b/src/client/java/com/tcm/MineTale/recipe/MineTaleRecipeBookComponent.java new file mode 100644 index 0000000..3b64073 --- /dev/null +++ b/src/client/java/com/tcm/MineTale/recipe/MineTaleRecipeBookComponent.java @@ -0,0 +1,80 @@ +package com.tcm.MineTale.recipe; + +import java.util.List; + +import com.tcm.MineTale.registry.ModRecipeDisplay; +import com.tcm.MineTale.util.Constants; + +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.screens.recipebook.GhostSlots; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.gui.screens.recipebook.RecipeCollection; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.RecipeBookMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.crafting.display.RecipeDisplay; + +public class MineTaleRecipeBookComponent extends RecipeBookComponent { + + // Standard button sprites (the "Filter" checkmark button) + protected static final WidgetSprites FILTER_BUTTON_SPRITES = new WidgetSprites( + Identifier.withDefaultNamespace("recipe_book/filter_enabled"), + Identifier.withDefaultNamespace("recipe_book/filter_disabled"), + Identifier.withDefaultNamespace("recipe_book/filter_enabled_focused"), + Identifier.withDefaultNamespace("recipe_book/filter_disabled_focused") + ); + + public MineTaleRecipeBookComponent(RecipeBookMenu recipeBookMenu, List list) { + super(recipeBookMenu, list); + } + + @Override + protected void selectMatchingRecipes(RecipeCollection recipeCollection, StackedItemContents stackedItemContents) { + // Force everything to be "selected" + // recipeCollection.selectRecipes(stackedItemContents, (recipeDisplay) -> true); + + recipeCollection.selectRecipes(stackedItemContents, (recipeDisplay) -> { + // Only allow recipes that use your custom Workbench display type + // This effectively filters out vanilla CraftingRecipeDisplays (the boats) + return recipeDisplay.type() == ModRecipeDisplay.WORKBENCH_TYPE; + }); + } + + + + @Override + protected WidgetSprites getFilterButtonTextures() { + // Returns the textures for the "Toggle craftable" button + return FILTER_BUTTON_SPRITES; + } + + @Override + protected boolean isCraftingSlot(Slot slot) { + return slot.index == Constants.INPUT_START || slot.index == Constants.INPUT_START + 1; + } + + @Override + protected Component getRecipeFilterName() { + // The text shown when hovering over the filter button + return Component.translatable("gui.recipebook.toggleRecipes.all"); + } + + @Override + protected void fillGhostRecipe(GhostSlots ghostSlots, RecipeDisplay recipeDisplay, ContextMap contextMap) { + // This places the faint "ghost" items in the workbench slots when hovering a recipe + // We use SlotDisplayContext.fromLevel(this.minecraft.level) to handle dynamic displays + // ghostSlots.setRecipe(recipeDisplay); + + // // We assume the first two slots of your menu are the inputs + // // Your AbstractWorkbenchContainerMenu adds input slots first (index 0 and 1) + // ghostSlots.addSlot(this.menu.slots.get(0), this.minecraft.level.registryAccess(), recipeDisplay.result()); + + // // If your custom recipe has specific inputs, you'd map them here. + // // For a generic implementation, we use the display's suggested placement: + // recipeDisplay.setupGhostSlots(ghostSlots, SlotDisplayContext.fromLevel(this.minecraft.level)); + } + +} \ No newline at end of file diff --git a/src/client/java/net/minecraft/client/gui/screens/recipebook/GhostSlotsProxy.java b/src/client/java/net/minecraft/client/gui/screens/recipebook/GhostSlotsProxy.java new file mode 100644 index 0000000..86205c1 --- /dev/null +++ b/src/client/java/net/minecraft/client/gui/screens/recipebook/GhostSlotsProxy.java @@ -0,0 +1,16 @@ +package net.minecraft.client.gui.screens.recipebook; + +import net.minecraft.world.inventory.Slot; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +public class GhostSlotsProxy { + public static void setInputProxy(GhostSlots ghostSlots, Slot slot, ContextMap contextMap, SlotDisplay display) { + // Because this class is in the same package, it can see protected methods! + ghostSlots.setInput(slot, contextMap, display); + } + + public static void setResultProxy(GhostSlots ghostSlots, Slot slot, ContextMap contextMap, SlotDisplay display) { + ghostSlots.setResult(slot, contextMap, display); + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/MineTale.java b/src/main/java/com/tcm/MineTale/MineTale.java index 4ed9e8e..739adbd 100644 --- a/src/main/java/com/tcm/MineTale/MineTale.java +++ b/src/main/java/com/tcm/MineTale/MineTale.java @@ -1,7 +1,7 @@ package com.tcm.MineTale; import net.fabricmc.api.ModInitializer; - +import net.fabricmc.fabric.api.recipe.v1.sync.RecipeSynchronization; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import org.slf4j.Logger; @@ -13,6 +13,7 @@ import com.tcm.MineTale.registry.ModEntityDataSerializers; import com.tcm.MineTale.registry.ModItems; import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.registry.ModRecipeDisplay; import com.tcm.MineTale.registry.ModRecipes; import static com.tcm.MineTale.item.ModCreativeTab.MINETALE_CREATIVE_TAB; @@ -34,17 +35,32 @@ public class MineTale implements ModInitializer { */ @Override public void onInitialize() { + // 1. Blocks first - They are the foundation ModBlocks.initialize(); - ModRecipes.initialize(); + + // 2. Items second - Many blocks have associated BlockItems + ModItems.initialize(); + + // 3. Block Entities third - They now have non-null Blocks to reference ModBlockEntities.initialize(); - ModMenuTypes.initialize(); + + // 4. Entities & Menus - These depend on the objects above ModEntities.initialize(); - ModItems.initialize(); + ModMenuTypes.initialize(); + + // 5. Recipes last - These depend on Items, Blocks, and Entities existing + ModRecipes.initialize(); + + // ADD THIS HERE + ModRecipeDisplay.initialize(); Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB, MINETALE_CREATIVE_TAB_KEY, MINETALE_CREATIVE_TAB); ModEntityDataSerializers.initialize(); + RecipeSynchronization.synchronizeRecipeSerializer(ModRecipes.FURNACE_SERIALIZER); + // This helps the search bar "see" items in your custom categories + LOGGER.info("Hello Fabric world!"); } } \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java b/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java index 3e4f01c..7bc40cd 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java @@ -37,18 +37,24 @@ public abstract class AbstractWorkbench exten protected final Supplier> blockEntityType; protected final boolean isWide; protected final boolean isTall; + protected int tier; - protected AbstractWorkbench(Properties properties, Supplier> supplier, boolean isWide, boolean isTall) { + protected AbstractWorkbench(Properties properties, Supplier> supplier, boolean isWide, boolean isTall, int tier) { super(properties); this.blockEntityType = supplier; this.isWide = isWide; this.isTall = isTall; + this.tier = tier; this.registerDefaultState(this.stateDefinition.any() .setValue(FACING, Direction.NORTH) .setValue(HALF, DoubleBlockHalf.LOWER) .setValue(TYPE, ChestType.SINGLE)); } + public int getTier() { + return this.tier; + } + @Override @Nullable public BlockState getStateForPlacement(BlockPlaceContext context) { @@ -144,29 +150,17 @@ protected void createBlockStateDefinition(StateDefinition.Builder ModBlockEntities.CAMPFIRE_WORKBENCH_BE, IS_WIDE, IS_TALL); + super(properties, () -> ModBlockEntities.CAMPFIRE_WORKBENCH_BE, IS_WIDE, IS_TALL, 1); } /** @@ -50,7 +50,7 @@ public CampfireWorkbench(Properties properties) { */ public CampfireWorkbench(Properties properties, Supplier> supplier) { // isWide = false, isTall = false (1x1 footprint) - super(properties, supplier, IS_WIDE, IS_TALL); + super(properties, supplier, IS_WIDE, IS_TALL, 1); } /** diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/FurnaceWorkbench.java b/src/main/java/com/tcm/MineTale/block/workbenches/FurnaceWorkbench.java index 207d91b..9cc2bfe 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/FurnaceWorkbench.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/FurnaceWorkbench.java @@ -1,12 +1,13 @@ package com.tcm.MineTale.block.workbenches; -import java.util.function.Supplier; - import org.jetbrains.annotations.Nullable; +import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; import com.tcm.MineTale.block.workbenches.entity.FurnaceWorkbenchEntity; -import com.tcm.MineTale.registry.ModBlockEntities; +import com.tcm.MineTale.registry.ModTiers; +import com.tcm.MineTale.registry.ModTiers.FurnaceTier; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; @@ -21,26 +22,33 @@ public class FurnaceWorkbench extends AbstractWorkbench private static final boolean IS_WIDE = true; private static final boolean IS_TALL = true; - public static final MapCodec CODEC = simpleCodec(FurnaceWorkbench::new); + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + instance.group( + // This handles the standard block properties + propertiesCodec(), + // This handles the tier (assuming FurnaceTier is a record/enum with its own codec) + Codec.INT.fieldOf("tier").forGetter(block -> block.getTier()) + ).apply(instance, (props, id) -> new FurnaceWorkbench(props, ModTiers.getTierFromInt(id))) + ); /** * Creates a FurnaceWorkbench using the default furnace workbench block entity type and a 2×2 footprint. * * @param properties block properties for this workbench */ - public FurnaceWorkbench(Properties properties) { - super(properties, () -> ModBlockEntities.FURNACE_WORKBENCH_BE, IS_WIDE, IS_TALL); + public FurnaceWorkbench(Properties properties, FurnaceTier tier) { + super(properties, () -> ModTiers.TIER_MAP.get(tier), IS_WIDE, IS_TALL, tier.id()); } - /** - * Creates a FurnaceWorkbench that uses a custom BlockEntityType supplier and a 2x2 footprint. - * - * @param properties block properties for this workbench - * @param supplier supplies the BlockEntityType to use for the workbench's master block entity - */ - public FurnaceWorkbench(Properties properties, Supplier> supplier) { - super(properties, supplier, IS_WIDE, IS_TALL); - } + // /** + // * Creates a FurnaceWorkbench that uses a custom BlockEntityType supplier and a 2x2 footprint. + // * + // * @param properties block properties for this workbench + // * @param supplier supplies the BlockEntityType to use for the workbench's master block entity + // */ + // public FurnaceWorkbench(Properties properties, Supplier> supplier) { + // super(properties, supplier, IS_WIDE, IS_TALL, 1); + // } /** * Ensures the block is rendered using its model so the 2x2 workbench model is visible. @@ -72,26 +80,13 @@ public RenderShape getRenderShape(BlockState state) { public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { // Only the Master block (Lower-Left) should tick to process smelting // This helper ensures the logic only runs on the Server side for our specific BE - return createTickerHelper(type, ModBlockEntities.FURNACE_WORKBENCH_BE, (lvl, pos, st, be) -> { + return createTickerHelper(type, ModTiers.TIER_MAP.get(ModTiers.getTierFromInt(this.tier)), (lvl, pos, st, be) -> { if (be instanceof FurnaceWorkbenchEntity furnace) { furnace.tick(lvl, pos, st); } }); } - /** - * Create the block entity for this block; only the master block of the multi-block workbench receives an entity. - * - * @return the created {@link BlockEntity} for the master block, or `null` if this position does not host an entity - */ - @Nullable - @Override - public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { - // AbstractWorkbench logic ensures only the Master block gets the entity. - // We override it here to point specifically to our Furnace entity. - return super.newBlockEntity(pos, state); - } - /** * Supply the codec used to serialize and deserialize this FurnaceWorkbench. * diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractFurnaceWorkbenchEntity.java b/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractFurnaceWorkbenchEntity.java new file mode 100644 index 0000000..020579d --- /dev/null +++ b/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractFurnaceWorkbenchEntity.java @@ -0,0 +1,207 @@ +package com.tcm.MineTale.block.workbenches.entity; + +import java.util.List; + +import com.mojang.serialization.Codec; +import com.tcm.MineTale.recipe.WorkbenchRecipe; +import com.tcm.MineTale.registry.ModRecipes; +import com.tcm.MineTale.util.Constants; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +public abstract class AbstractFurnaceWorkbenchEntity extends AbstractWorkbenchEntity { + private int cookTime; + private int cookTimeTotal = 200; + private int fuelTime; + + // Defined by the subclass via constructor + protected final int inputEnd; + protected final int outputEnd; + + public AbstractFurnaceWorkbenchEntity(BlockEntityType type, BlockPos pos, BlockState state, int inputEnd, int outputEnd) { + super(type, pos, state); + this.inputEnd = inputEnd; + this.outputEnd = outputEnd; + } + + // --- Getters and Setters --- + public int getFuelTime() { return this.fuelTime; } + public void setFuelTime(int t) { this.fuelTime = t; } + public int getCookTime() { return this.cookTime; } + public void setCookTime(int t) { this.cookTime = t; } + public int getCookTimeTotal() { return this.cookTimeTotal; } + public void setCookTimeTotal(int t) { this.cookTimeTotal = t; } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) return; + + boolean changed = false; + + // 1. Shift queue forward so the next item is ready to smelt + if (shiftQueueForward()) { + changed = true; + } + + ItemStack activeInput = inventory.getItem(Constants.INPUT_START); + ItemStack fuel = inventory.getItem(Constants.FUEL_SLOT); + + // 2. Processing logic (Pulling from chests removed from here) + if (canSmelt(activeInput)) { + int speedBoost = (this.tier - 1) * 50; + int currentTotal = Math.max(20, this.cookTimeTotal - speedBoost); + + if (this.fuelTime > 0 || !fuel.isEmpty()) { + if (this.fuelTime <= 0 && consumeFuel(fuel)) { + changed = true; + } + + if (this.fuelTime > 0) { + this.fuelTime--; + this.cookTime++; + if (this.cookTime >= currentTotal) { + smeltItem(activeInput); + this.cookTime = 0; + changed = true; + } + } + } + } else { + this.cookTime = 0; + } + + if (changed) setChanged(); + } + + /** + * Iterates through the queue range. If a slot is empty, it pulls the item + * from the slot behind it. + */ + private boolean shiftQueueForward() { + boolean moved = false; + // Start from the front and pull from the back + for (int i = Constants.INPUT_START; i < this.inputEnd; i++) { + ItemStack current = inventory.getItem(i); + ItemStack next = inventory.getItem(i + 1); + + if (current.isEmpty() && !next.isEmpty()) { + inventory.setItem(i, next.copy()); + inventory.setItem(i + 1, ItemStack.EMPTY); + moved = true; + } + } + return moved; + } + + private void smeltItem(ItemStack input) { + ItemStack result = isWood(input) ? new ItemStack(Items.CHARCOAL) : new ItemStack(Items.COPPER_INGOT); + + // Uses your existing findOutputSlot logic + int outputSlot = this.findOutputSlot(result, inventory, this.inputEnd + 1, this.outputEnd); + if (outputSlot == -1) return; + + ItemStack output = inventory.getItem(outputSlot); + if (output.isEmpty()) { + inventory.setItem(outputSlot, result.copy()); + } else if (ItemStack.isSameItem(output, result)) { + output.grow(result.getCount()); + } + input.shrink(1); + } + + /** + * CALLED BY SCREENHANDLER / RECIPE BOOK + * Searches nearby chests for items matching the selected recipe and + * fills any empty slots in the queue. + */ + public void fulfillRecipeFromNearby(WorkbenchRecipe recipe) { + if (this.level == null || this.level.isClientSide() || recipe.ingredients().isEmpty()) return; + + // Assuming your WorkbenchRecipe has a method to get its input ingredient + // If it's a standard furnace-style recipe, it likely has one ingredient. + Ingredient ingredient = recipe.ingredients().get(0); + List nearby = this.getNearbyInventories(); + + // Iterate through our queue slots + for (int slot = Constants.INPUT_START; slot <= this.inputEnd; slot++) { + // Only try to fill if the slot is currently empty + if (inventory.getItem(slot).isEmpty()) { + + // Look through nearby chests + for (Container chest : nearby) { + for (int i = 0; i < chest.getContainerSize(); i++) { + ItemStack stackInChest = chest.getItem(i); + + if (!stackInChest.isEmpty() && ingredient.test(stackInChest)) { + // Take 1 (or a full stack if you prefer) and put it in the queue + inventory.setItem(slot, stackInChest.split(1)); + chest.setChanged(); + this.setChanged(); + + // Break to next queue slot once this one is filled + break; + } + } + // If we filled the slot, stop looking at other chests for this specific slot + if (!inventory.getItem(slot).isEmpty()) break; + } + } + } + } + + private boolean consumeFuel(ItemStack fuel) { + if (fuel.is(Items.STICK) || fuel.is(Items.STRING) || isWood(fuel)) { + this.fuelTime = 100; + fuel.shrink(1); + return true; + } + return false; + } + + private boolean canSmelt(ItemStack input) { + return !input.isEmpty() && (isOre(input) || isWood(input)); + } + + private boolean isOre(ItemStack stack) { return stack.is(Items.RAW_COPPER); } + private boolean isWood(ItemStack stack) { return stack.is(ItemTags.LOGS_THAT_BURN); } + + @Override + protected void saveAdditional(ValueOutput valueOutput) { + super.saveAdditional(valueOutput); + valueOutput.store("CookTime", Codec.INT, this.cookTime); + valueOutput.store("FuelTime", Codec.INT, this.fuelTime); + valueOutput.store("WorkbenchTier", Codec.INT, this.tier); + } + + @Override + protected void loadAdditional(ValueInput valueInput) { + super.loadAdditional(valueInput); + this.cookTime = valueInput.read("CookTime", Codec.INT).orElse(0); + this.fuelTime = valueInput.read("FuelTime", Codec.INT).orElse(0); + this.tier = valueInput.read("WorkbenchTier", Codec.INT).orElse(1); + } + + @Override + public RecipeType getWorkbenchRecipeType() { + return ModRecipes.FURNACE_TYPE; + } + + @Override + protected boolean hasFuel() { + if (this.level == null) return false; + BlockState state = this.level.getBlockState(this.worldPosition); + boolean isLit = state.hasProperty(BlockStateProperties.LIT) && state.getValue(BlockStateProperties.LIT); + return isLit || this.fuelTime > 0; + } +} diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractWorkbenchEntity.java b/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractWorkbenchEntity.java index 8e12a1e..21cc6a6 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractWorkbenchEntity.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/entity/AbstractWorkbenchEntity.java @@ -27,11 +27,11 @@ import net.minecraft.world.level.block.state.BlockState; public abstract class AbstractWorkbenchEntity extends BlockEntity implements MenuProvider { - protected int tier = 1; + protected int tier; protected double scanRadius = 5.0; // Slot Mapping: 0-1 Inputs, 2 Fuel, 3-6 Outputs - protected final SimpleContainer inventory = new SimpleContainer(Constants.TOTAL_SLOTS); + protected final SimpleContainer inventory = new SimpleContainer(7); protected int progress = 0; protected int maxProgress = 200; @@ -72,19 +72,10 @@ public static void tick(Level level, BlockPos pos, BlockState state, AbstractWor // 1. Create the input wrapper using the internal SimpleContainer // Slot 1 = Input A, Slot 2 = Input B WorkbenchRecipeInput input = new WorkbenchRecipeInput( - entity.inventory.getItem(Constants.INPUT_1), - entity.inventory.getItem(Constants.INPUT_2) + entity.inventory.getItem(Constants.INPUT_START), + entity.inventory.getItem(2) ); - // DEBUG 1: Is the machine even seeing the pork? - if (!entity.inventory.getItem(Constants.INPUT_1).isEmpty()) { - System.out.println("Slot 1 (Input) contains: " + entity.inventory.getItem(Constants.INPUT_1).getItem().toString()); - } - - if (!entity.inventory.getItem(Constants.FUEL_SLOT).isEmpty()) { - System.out.println("Slot 0 (Fuel) contains: " + entity.inventory.getItem(Constants.FUEL_SLOT).getItem().toString()); - } - // 2. Fetch the RecipeManager from the server if (level.getServer() == null) return; var recipeManager = level.getServer().getRecipeManager(); @@ -159,7 +150,7 @@ public static void tick(Level level, BlockPos pos, BlockState state, AbstractWor public boolean canFitOutputs(List results) { for (ItemStack result : results) { // If we can't find a home for even one of the results, return false - if (findOutputSlot(result, Constants.OUTPUT_START, Constants.OUTPUT_END) == -1) { + if (findOutputSlot(result, 3, 6) == -1) { return false; } } @@ -220,15 +211,15 @@ public boolean isEmpty() { */ protected void craft(WorkbenchRecipe recipe) { // 1. Consume 1 from each ingredient slot (Slots 1 and 2) - this.removeItem(Constants.INPUT_1, 1); - this.removeItem(Constants.INPUT_2, 1); + this.removeItem(Constants.INPUT_START, 1); + this.removeItem(2, 1); // 2. Distribute results from the recipe for (ItemStack result : recipe.results()) { if (result.isEmpty()) continue; // Use the constants for the output range (3 to 6) - int slot = findOutputSlot(result, Constants.OUTPUT_START, Constants.OUTPUT_END); + int slot = findOutputSlot(result, 3, 6); if (slot != -1) { ItemStack existing = getItem(slot); diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/entity/CampfireWorkbenchEntity.java b/src/main/java/com/tcm/MineTale/block/workbenches/entity/CampfireWorkbenchEntity.java index 95d27c3..c6b7216 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/entity/CampfireWorkbenchEntity.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/entity/CampfireWorkbenchEntity.java @@ -178,7 +178,7 @@ protected void loadAdditional(ValueInput valueInput) { */ @Override public @Nullable AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) { - return new CampfireWorkbenchMenu(syncId, playerInventory, this.inventory, this.data); + return new CampfireWorkbenchMenu(syncId, playerInventory, this.inventory, this.data, this); } /** diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/entity/FurnaceWorkbenchEntity.java b/src/main/java/com/tcm/MineTale/block/workbenches/entity/FurnaceWorkbenchEntity.java index f65bc7b..e617bb5 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/entity/FurnaceWorkbenchEntity.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/entity/FurnaceWorkbenchEntity.java @@ -1,37 +1,18 @@ package com.tcm.MineTale.block.workbenches.entity; -import java.util.List; - import org.jspecify.annotations.Nullable; -import com.mojang.serialization.Codec; import com.tcm.MineTale.block.workbenches.menu.FurnaceWorkbenchMenu; -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 com.tcm.MineTale.registry.ModTiers; import net.minecraft.core.BlockPos; -import net.minecraft.tags.ItemTags; -import net.minecraft.world.Container; 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.Items; -import net.minecraft.world.item.crafting.RecipeType; -import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.properties.BlockStateProperties; -import net.minecraft.world.level.storage.ValueInput; -import net.minecraft.world.level.storage.ValueOutput; - -public class FurnaceWorkbenchEntity extends AbstractWorkbenchEntity { - private int cookTime; - private int cookTimeTotal = 200; - private int fuelTime; +public class FurnaceWorkbenchEntity extends AbstractFurnaceWorkbenchEntity { protected final ContainerData data = new ContainerData() { /** * Retrieves an internal data value by index for UI synchronization. @@ -43,10 +24,10 @@ public class FurnaceWorkbenchEntity extends AbstractWorkbenchEntity { @Override public int get(int index) { return switch (index) { - case 0 -> fuelTime; + case 0 -> getFuelTime(); case 1 -> 100; // Fuel total - case 2 -> cookTime; - case 3 -> cookTimeTotal; + case 2 -> getCookTime(); + case 3 -> getCookTimeTotal(); default -> 0; }; } @@ -67,8 +48,8 @@ public int get(int index) { @Override public void set(int index, int value) { switch (index) { - case 0 -> fuelTime = value; - case 2 -> cookTime = value; + case 0 -> setFuelTime(value); + case 2 -> setCookTime(value); } } @@ -84,211 +65,24 @@ public int getCount() { }; /** - * Creates a new FurnaceWorkbenchEntity at the specified position and block state. - * - * Initializes the entity and sets the default nearby-inventory scan radius to 8.0. + * Creates a new FurnaceWorkbenchEntity with dynamic tier data. * + * @param tier the record containing speed, radius, and other stats * @param pos the block position of the entity * @param state the block state at that position */ - public FurnaceWorkbenchEntity(BlockPos pos, BlockState state) { - super(ModBlockEntities.FURNACE_WORKBENCH_BE, pos, state); - this.scanRadius = 8.0; - } - - /** - * Performs server-side per-tick processing for the furnace workbench: attempts to pull input items from nearby - * containers, manages fuel consumption, advances smelting progress according to workbench tier, and produces output - * when a smelt cycle completes. - * - *

Behavioral notes: - * - Runs only on the server side. - * - When input is empty, attempts to pull a single eligible input item from nearby inventories once every 20 game ticks. - * - Workbench tier reduces the required cook duration; cooking advances while fuel is available and is reset when smelting is not possible. - * - Consumes fuel items to refill internal fuel time, decrements fuel time each tick, increments cook progress, and invokes smeltItem(...) when a cycle finishes. - * - Marks the block entity changed if any inventory or internal state is modified.

- * - * @param level the world in which the workbench exists - * @param pos the block position of the workbench - * @param state the current block state of the workbench - */ - public void tick(Level level, BlockPos pos, BlockState state) { - if (level.isClientSide()) return; - - boolean changed = false; - ItemStack fuel = inventory.getItem(Constants.FUEL_SLOT); - ItemStack input = !inventory.getItem(Constants.INPUT_1).isEmpty() - ? inventory.getItem(Constants.INPUT_1) - : inventory.getItem(Constants.INPUT_2); - - // TRAIT: Streamline crafting by pulling from nearby chests if input is empty - if (input.isEmpty() && level.getGameTime() % 20 == 0) { - pullFromNearbyChests(); - } - - if (canSmelt(input)) { - // TRAIT: Upgrade System - Higher tier = faster smelting - // Tier 1: 200 ticks, Tier 2: 150 ticks, Tier 3: 100 ticks... - int speedBoost = (this.tier - 1) * 50; - int currentTotal = Math.max(20, this.cookTimeTotal - speedBoost); - - if (fuelTime > 0 || !fuel.isEmpty()) { - if (fuelTime <= 0 && consumeFuel(fuel)) { - changed = true; - } - - if (fuelTime > 0) { - fuelTime--; - cookTime++; - if (cookTime >= currentTotal) { - smeltItem(input); - cookTime = 0; - changed = true; - } - } - } - } else { - cookTime = 0; - } - - if (changed) setChanged(); - } - - /** - * Determines whether the provided stack is a valid smelting input (ore or log). - * - * @param input the item stack to test - * @return `true` if the stack represents an ore or a log, `false` otherwise - */ - private boolean canSmelt(ItemStack input) { - if (input.isEmpty()) return false; - // Logic: Check if it's an ore (Copper to Adamantite) or Logs for Charcoal - return isOre(input) || isWood(input); - } - - /** - * Consume one unit of the provided fuel item and set the internal fuel timer when the item is an accepted fuel. - * - * Accepted fuels: sticks, string (fibres), or any item recognized as wood. - * - * @param fuel the ItemStack to attempt to consume; one item will be removed if accepted - * @return `true` if a fuel unit was consumed and the internal fuel time was set to 100, `false` otherwise - */ - private boolean consumeFuel(ItemStack fuel) { - // TRAIT: Use fibres (string), sticks, or logs - if (fuel.is(Items.STICK) || fuel.is(Items.STRING) || isWood(fuel)) { - this.fuelTime = 100; // Assign burn time - fuel.shrink(1); - return true; - } - return false; - } - - /** - * Smelts a single input item into its output and deposits the result into an available output slot. - * - *

If the input is wood, produces charcoal; otherwise produces a copper ingot (placeholder for ore-to-ingot mapping). - * The method finds a suitable output slot and either places the result there or increases the existing stack; if no - * output slot is available the method does nothing. The input stack is reduced by one on successful smelting. - * - * @param input the ItemStack to smelt; one item will be consumed from this stack when smelting occurs - */ - private void smeltItem(ItemStack input) { - ItemStack result; - // TRAIT: Logs yield Charcoal - if (isWood(input)) { - result = new ItemStack(Items.CHARCOAL); - } else { - // Placeholder: Replace with your actual Ore-to-Ingot logic - result = new ItemStack(Items.COPPER_INGOT); - } - - int outputSlot = this.findOutputSlot(result, inventory, Constants.OUTPUT_START, Constants.OUTPUT_END); - if (outputSlot == -1) return; - ItemStack output = inventory.getItem(outputSlot); - - if (output.isEmpty()) { - inventory.setItem(outputSlot, result.copy()); - } else if (ItemStack.isSameItem(output, result)) { - output.grow(result.getCount()); - } - input.shrink(1); - } - - /** - * Attempts to move a single ore or wood item from nearby inventories into this entity's input slots. - * - * If INPUT_1 is empty that slot is filled first; otherwise INPUT_2 is used. If neither input slot is - * available or no matching item is found, the method does nothing. When an item is moved, the source - * container is marked changed. - */ - private void pullFromNearbyChests() { - List nearby = this.getNearbyInventories(); - for (Container chest : nearby) { - for (int i = 0; i < chest.getContainerSize(); i++) { - ItemStack stack = chest.getItem(i); - if (isOre(stack) || isWood(stack)) { - int inputSlot = -1; - if (inventory.getItem(Constants.INPUT_1).isEmpty()) { - inputSlot = Constants.INPUT_1; - } else if (inventory.getItem(Constants.INPUT_2).isEmpty()) { - inputSlot = Constants.INPUT_2; - } - if (inputSlot == -1) return; - inventory.setItem(inputSlot, stack.split(1)); - chest.setChanged(); - return; - } - } - } - } - - /** - * Determines whether the provided item stack is a supported ore. - * - * @param stack the item stack to test - * @return `true` if the stack is a supported ore (currently `Items.RAW_COPPER`), `false` otherwise - */ - private boolean isOre(ItemStack stack) { return stack.is(Items.RAW_COPPER); /* Add more ores */ } - /** - * Determines whether the given item stack represents a wood log item. - * - * @param stack the item stack to inspect - * @return `true` if the stack's item is a wood log, `false` otherwise - */ - private boolean isWood(ItemStack stack) { return stack.is(ItemTags.LOGS_THAT_BURN); } - - /** - * Persist entity-specific state into the provided ValueOutput. - * - * Stores the workbench's tier as "WorkbenchTier" and its scan radius as "ScanRadius" - * using type-safe Codecs. - * - * @param valueOutput the output writer used to serialize 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); - } + public FurnaceWorkbenchEntity(ModTiers.FurnaceTier tier, BlockPos pos, BlockState state, int inputEnd, int outputEnd) { + // Dynamically fetch the BlockEntityType from your registry map using the tier key + super(ModTiers.TIER_MAP.get(tier), pos, state, inputEnd, outputEnd); + this.tier = tier.id(); + + // You can now set scanRadius dynamically from the record + // or keep it a default value. + this.scanRadius = tier.scanRadius(); - /** - * Restores workbench-specific state from persistent storage and applies defaults when keys are absent. - * - * Delegates to the superclass load logic, then reads: - * - "WorkbenchTier" (int) into {@code tier}, defaulting to {@code 1} if missing. - * - "ScanRadius" (double) into {@code scanRadius}, defaulting to {@code 5.0} if missing. - */ - @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(5.0); + this.setCookTimeTotal(tier.cookTime()); } - + /** * Create a container menu that allows a player to interact with this furnace workbench. * @@ -299,36 +93,6 @@ protected void loadAdditional(ValueInput valueInput) { */ @Override public @Nullable AbstractContainerMenu createMenu(int syncId, Inventory playerInventory, Player player) { - return new FurnaceWorkbenchMenu(syncId, playerInventory, this.inventory, this.data); - } - - /** - * Identifies the recipe type used by this furnace-style workbench. - * - * @return the RecipeType for furnace workbench recipes (ModRecipes.FURNACE_TYPE) - */ - @Override - public RecipeType getWorkbenchRecipeType() { - return ModRecipes.FURNACE_TYPE; - } - - /** - * Checks whether the workbench is lit and has a fuel item available. - * - * Returns false if the block entity is not attached to a level. - * - * @return `true` if the block's `LIT` property is present and true and the configured fuel slot is non-empty, `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 isLit && hasFuelItem; + return new FurnaceWorkbenchMenu(syncId, playerInventory, this.inventory, this.data, this); } } \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/menu/AbstractWorkbenchContainerMenu.java b/src/main/java/com/tcm/MineTale/block/workbenches/menu/AbstractWorkbenchContainerMenu.java index 01cee3c..01967b7 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/menu/AbstractWorkbenchContainerMenu.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/menu/AbstractWorkbenchContainerMenu.java @@ -1,24 +1,39 @@ 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.WorkbenchRecipe; +import com.tcm.MineTale.recipe.WorkbenchRecipeInput; import com.tcm.MineTale.util.Constants; +import net.minecraft.recipebook.ServerPlaceRecipe; +import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.ItemTags; import net.minecraft.world.Container; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.entity.player.StackedItemContents; import net.minecraft.world.inventory.ContainerData; import net.minecraft.world.inventory.FurnaceResultSlot; import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.RecipeBookMenu; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.StackedContentsCompatible; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.RecipeHolder; -public abstract class AbstractWorkbenchContainerMenu extends AbstractContainerMenu { - private final Container container; +public abstract class AbstractWorkbenchContainerMenu extends RecipeBookMenu implements StackedContentsCompatible { + protected final Container container; private final ContainerData data; + + protected final int inputEnd; + protected final int outputEnd; + + protected final Inventory playerInventory; /** * Creates a workbench container menu backed by the given inventory and sync data, sets up slots @@ -32,13 +47,17 @@ public abstract class AbstractWorkbenchContainerMenu extends AbstractContainerMe * @param containerDataSize expected size of {@code data}; validated by this constructor * @param playerInventory the player's inventory used to add player slots and to identify the player for result slots */ - public AbstractWorkbenchContainerMenu(@Nullable MenuType menuType, int syncId, Container container, ContainerData data, int containerSize, int containerDataSize, Inventory playerInventory) { + public AbstractWorkbenchContainerMenu(@Nullable MenuType menuType, int syncId, Container container, ContainerData data, int containerDataSize, Inventory playerInventory, int inputEnd, int outputEnd) { super(menuType, syncId); - checkContainerSize(container, containerSize); + this.outputEnd = outputEnd; + this.inputEnd = inputEnd; + + checkContainerSize(container, outputEnd + 1); checkContainerDataCount(data, containerDataSize); this.container = container; this.data = data; + this.playerInventory = playerInventory; container.startOpen(playerInventory.player); @@ -57,14 +76,29 @@ public boolean mayPlace(ItemStack stack) { }); // 2. Two Input Slots (Stacked on the left) - this.addSlot(new Slot(container, Constants.INPUT_1, 35, 17)); //LEFT - this.addSlot(new Slot(container, Constants.INPUT_2, 53, 17)); //RIGHT + this.addSlot(new Slot(container, Constants.INPUT_START, 35, 17) { + @Override + public boolean mayPlace(ItemStack stack) { + // If this logic is too restrictive (e.g., checking for fuel only), + // the Recipe Book simulation will fail. + return true; + } + }); //LEFT + this.addSlot(new Slot(container, this.inputEnd, 53, 17) { + @Override + public boolean mayPlace(ItemStack stack) { + // If this logic is too restrictive (e.g., checking for fuel only), + // the Recipe Book simulation will fail. + return true; + } + }); //RIGHT + // 3. Four Output Slots (2x2 Grid on the right) - this.addSlot(new FurnaceResultSlot(playerInventory.player, container, Constants.OUTPUT_START, 107, 26)); //TOP LEFT - this.addSlot(new FurnaceResultSlot(playerInventory.player, container, Constants.OUTPUT_START + 1, 125, 26)); //TOP RIGHT - this.addSlot(new FurnaceResultSlot(playerInventory.player, container, Constants.OUTPUT_END - 1, 107, 44)); //BOTTOM LEFT - this.addSlot(new FurnaceResultSlot(playerInventory.player, container, Constants.OUTPUT_END, 125, 44)); //BOTTOM RIGHT + this.addSlot(new FurnaceResultSlot(playerInventory.player, container, this.inputEnd + 1, 107, 26)); //TOP LEFT + this.addSlot(new FurnaceResultSlot(playerInventory.player, container, this.inputEnd + 2, 125, 26)); //TOP RIGHT + this.addSlot(new FurnaceResultSlot(playerInventory.player, container, this.outputEnd - 1, 107, 44)); //BOTTOM LEFT + this.addSlot(new FurnaceResultSlot(playerInventory.player, container, this.outputEnd, 125, 44)); //BOTTOM RIGHT // --- PLAYER INVENTORY --- addPlayerInventory(playerInventory); @@ -146,45 +180,41 @@ public int getBurnProgress() { return this.data.get(0) * 13 / i; // fuelTime } - /** - * Performs a shift-click transfer between this container and the player's inventory. - * - * Moves the clicked stack into the player's inventory if it came from the container, or into the appropriate container - * slots if it came from the player's inventory. Fuel items are moved to the fuel slot; all other items are moved to the - * input slots. If the transfer cannot be completed, no changes are applied to the source slot. - * - * @param player the player performing the transfer - * @param index the index of the slot that was shift-clicked - * @return the original ItemStack from the clicked slot, or ItemStack.EMPTY if the transfer failed - */ @Override public ItemStack quickMoveStack(Player player, int index) { ItemStack itemStack = ItemStack.EMPTY; Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { ItemStack itemStack2 = slot.getItem(); itemStack = itemStack2.copy(); - // From Furnace to Player - int containerSlots = Constants.TOTAL_SLOTS; - int playerStart = containerSlots; - int playerEnd = playerStart + 36; + int workbenchSlotsEnd = this.outputEnd + 1; - // From Furnace to Player - if (index < containerSlots) { - if (!this.moveItemStackTo(itemStack2, playerStart, playerEnd, true)) { + // CASE 1: Moving from Workbench to Player Inventory + if (index < workbenchSlotsEnd) { + // Try to move to player inventory (indices 7 to 43) + // Use reverse = true to fill the hotbar last (standard vanilla behavior) + if (!this.moveItemStackTo(itemStack2, workbenchSlotsEnd, this.slots.size(), true)) { return ItemStack.EMPTY; } } - // From Player to Furnace + // CASE 2: Moving from Player Inventory to Workbench else { - // If it's fuel, try fuel slot - if (isFuel(itemStack2)) { - if (!this.moveItemStackTo(itemStack2, Constants.FUEL_SLOT, Constants.FUEL_SLOT + 1, false)) return ItemStack.EMPTY; - } - // Otherwise, try inputs - else if (!this.moveItemStackTo(itemStack2, Constants.INPUT_1, Constants.INPUT_2 + 1, false)) { - return ItemStack.EMPTY; + if (this.isFuel(itemStack2)) { + // 1. Try the Fuel Slot (Index 0) + if (!this.moveItemStackTo(itemStack2, Constants.FUEL_SLOT, Constants.FUEL_SLOT + 1, false)) { + // 2. If fuel is full, try the Input slots (Indices 1 to 3) as backup + if (!this.moveItemStackTo(itemStack2, Constants.INPUT_START, this.inputEnd + 1, false)) { + return ItemStack.EMPTY; + } + } + } else { + // 3. Not fuel? Go straight to Input slots (Indices 1 to 3) + // This ensures Slot 1 is checked BEFORE Slot 2 + if (!this.moveItemStackTo(itemStack2, Constants.INPUT_START, this.inputEnd + 1, false)) { + return ItemStack.EMPTY; + } } } @@ -193,7 +223,92 @@ else if (!this.moveItemStackTo(itemStack2, Constants.INPUT_1, Constants.INPUT_2 } else { slot.setChanged(); } + + if (itemStack2.getCount() == itemStack.getCount()) { + return ItemStack.EMPTY; + } + + slot.onTake(player, itemStack2); } + return itemStack; } + + public abstract @Nullable AbstractWorkbenchEntity getBlockEntity(); + + @Override + public RecipeBookMenu.PostPlaceAction handlePlacement(boolean placeAll, boolean isSpecial, RecipeHolder recipe, ServerLevel serverLevel, Inventory inventory + ) { + if (recipe.value() instanceof WorkbenchRecipe) { + @SuppressWarnings("unchecked") + RecipeHolder castRecipe = (RecipeHolder) recipe; + // 2. Call the static placeRecipe method + return ServerPlaceRecipe.placeRecipe( + new ServerPlaceRecipe.CraftingMenuAccess() { + @Override + public void fillCraftSlotsStackedContents(StackedItemContents contents) { + AbstractWorkbenchContainerMenu.this.fillCraftSlotsStackedContents(contents); + } + + @Override + public void clearCraftingContent() { + // Instead of setting to EMPTY, return items to player inventory + // This allows the Recipe Book to 'refill' or 'stack' properly + for (int i : new int[]{Constants.INPUT_START, AbstractWorkbenchContainerMenu.this.inputEnd}) { + ItemStack stack = AbstractWorkbenchContainerMenu.this.getSlot(i).getItem(); + if (!stack.isEmpty()) { + AbstractWorkbenchContainerMenu.this.playerInventory.placeItemBackInInventory(stack); + AbstractWorkbenchContainerMenu.this.getSlot(i).set(ItemStack.EMPTY); + } + } + } + + @Override + public boolean recipeMatches(RecipeHolder holder) { + return holder.value().matches( + AbstractWorkbenchContainerMenu.this.createRecipeInput(), + serverLevel + ); + } + }, + 1, // Grid Width + 1, // Grid Height + // FIX: Pass Slots 1 and 2 here. + // If Constants.INPUT_START is 1 and inputEnd is 2, this is correct: + List.of(this.getSlot(Constants.INPUT_START), this.getSlot(this.inputEnd)), + // Result Slots (3, 4, 5, 6) + List.of(this.getSlot(this.inputEnd + 1), this.getSlot(this.inputEnd + 2), + this.getSlot(this.outputEnd - 1), this.getSlot(this.outputEnd)), + inventory, + castRecipe, + placeAll, + false + ); + } + + return PostPlaceAction.NOTHING; + } + + @Override + public void fillStackedContents(StackedItemContents contents) { + // You MUST manually add the items from your SimpleContainer + // to the contents for the recipe book to "simulate" correctly. + for (int i = Constants.INPUT_START; i <= this.inputEnd; i++) { + contents.accountStack(this.container.getItem(i)); + } + } + + @Override + public void fillCraftSlotsStackedContents(StackedItemContents contents) { + // 1. Tell the server what is in the player's pockets + this.playerInventory.fillStackedContents(contents); + + // 2. Tell the server what is already in the workbench slots + // This allows the server to 'add' to the existing count + for (int i = Constants.INPUT_START; i <= this.inputEnd; i++) { + contents.accountStack(this.container.getItem(i)); + } + } + + public abstract WorkbenchRecipeInput createRecipeInput(); } \ 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 6342188..0245b26 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 @@ -1,17 +1,26 @@ package com.tcm.MineTale.block.workbenches.menu; +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.util.Constants; import net.minecraft.world.Container; 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.inventory.StackedContentsCompatible; public class CampfireWorkbenchMenu extends AbstractWorkbenchContainerMenu { - private static final int containerSize = Constants.TOTAL_SLOTS; private static final int containerDataSize = 4; + + @Nullable + private final AbstractWorkbenchEntity blockEntity; /** * Creates a CampfireWorkbenchMenu using default internal storage and data containers. @@ -23,7 +32,7 @@ public class CampfireWorkbenchMenu extends AbstractWorkbenchContainerMenu { * @param playerInventory the player's inventory interacting with this menu */ public CampfireWorkbenchMenu(int syncId, Inventory playerInventory) { - this(syncId, playerInventory, new SimpleContainer(containerSize), new SimpleContainerData(containerDataSize)); + this(syncId, playerInventory, new SimpleContainer(7), new SimpleContainerData(containerDataSize), null); } /** @@ -34,7 +43,37 @@ public CampfireWorkbenchMenu(int syncId, Inventory playerInventory) { * @param container the backing container for the workbench slots * @param data the container data used for syncing additional numeric state */ - public CampfireWorkbenchMenu(int syncId, Inventory playerInventory, Container container, ContainerData data) { - super(ModMenuTypes.CAMPFIRE_WORKBENCH_MENU, syncId, container, data, containerSize, containerDataSize, playerInventory); + 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); + this.blockEntity = blockEntity; + } + + @Override + public @Nullable AbstractWorkbenchEntity getBlockEntity() { + return this.blockEntity; + } + + @Override + public void fillCraftSlotsStackedContents(StackedItemContents stackedItemContents) { + // This is vital for the recipe book to "see" what is currently in your furnace. + // It allows the book to calculate if you have enough items to craft more. + if (this.container instanceof StackedContentsCompatible compatible) { + compatible.fillStackedContents(stackedItemContents); + } + } + + @Override + public RecipeBookType getRecipeBookType() { + return RecipeBookType.CRAFTING; + } + + @Override + public WorkbenchRecipeInput createRecipeInput() { + // We grab the items currently sitting in the container at indices 0 and 1 + // These correspond to the "Left" and "Right" input slots added in your constructor + return new WorkbenchRecipeInput( + this.container.getItem(Constants.INPUT_START), + this.container.getItem(this.inputEnd) + ); } } \ 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 dc45ec1..2bf5778 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 @@ -1,17 +1,26 @@ package com.tcm.MineTale.block.workbenches.menu; +import org.jspecify.annotations.Nullable; + +import com.tcm.MineTale.block.workbenches.entity.AbstractFurnaceWorkbenchEntity; +import com.tcm.MineTale.recipe.WorkbenchRecipeInput; import com.tcm.MineTale.registry.ModMenuTypes; +import com.tcm.MineTale.util.Constants; import net.minecraft.world.Container; 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.inventory.StackedContentsCompatible; public class FurnaceWorkbenchMenu extends AbstractWorkbenchContainerMenu { - private static final int containerSize = 7; private static final int containerDataSize = 4; + private final AbstractFurnaceWorkbenchEntity blockEntity; + /** * Creates a client-side FurnaceWorkbenchMenu with a new internal container and container data. * @@ -19,19 +28,43 @@ public class FurnaceWorkbenchMenu extends AbstractWorkbenchContainerMenu { * @param playerInventory the player's inventory to attach to this menu */ public FurnaceWorkbenchMenu(int syncId, Inventory playerInventory) { - this(syncId, playerInventory, new SimpleContainer(containerSize), new SimpleContainerData(containerDataSize)); + this(syncId, playerInventory, new SimpleContainer(6 + 1), new SimpleContainerData(containerDataSize), null); } - /** - * Initializes a server-side furnace-workbench menu for a player, setting up the container slots - * and attaching progress synchronization data. - * - * @param syncId the window synchronization id assigned by the server - * @param playerInventory the player's Inventory used to populate player inventory and hotbar slots - * @param container the backing container (expected size 7) that provides the workbench slots - * @param data the ContainerData (expected count 4) used to sync cook and burn progress - */ - public FurnaceWorkbenchMenu(int syncId, Inventory playerInventory, Container container, ContainerData data) { - super(ModMenuTypes.FURNACE_WORKBENCH_MENU, syncId, container, data, containerSize, containerDataSize, playerInventory); + public FurnaceWorkbenchMenu(int syncId, Inventory playerInventory, Container container, ContainerData data, @Nullable AbstractFurnaceWorkbenchEntity blockEntity) { + super(ModMenuTypes.FURNACE_WORKBENCH_MENU, syncId, container, data, containerDataSize, playerInventory, 2, 6); + this.blockEntity = blockEntity; + } + + @Override + public @Nullable AbstractFurnaceWorkbenchEntity getBlockEntity() { + // Return the block entity instance you passed into this menu's constructor + return this.blockEntity; + } + + @Override + public void fillCraftSlotsStackedContents(StackedItemContents stackedItemContents) { + // This is vital for the recipe book to "see" what is currently in your furnace. + // It allows the book to calculate if you have enough items to craft more. + if (this.container instanceof StackedContentsCompatible compatible) { + compatible.fillStackedContents(stackedItemContents); + } + } + + @Override + public RecipeBookType getRecipeBookType() { + // Tells the game which tab/category of the recipe book to save your settings under. + // Even though it's a custom furnace, using FURNACE ensures it behaves like one. + return RecipeBookType.FURNACE; + } + + @Override + public WorkbenchRecipeInput createRecipeInput() { + // We grab the items currently sitting in the container at indices 0 and 1 + // These correspond to the "Left" and "Right" input slots added in your constructor + return new WorkbenchRecipeInput( + this.container.getItem(Constants.INPUT_START), + this.container.getItem(this.inputEnd) + ); } } \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/datagen/ModRecipeProvider.java b/src/main/java/com/tcm/MineTale/datagen/ModRecipeProvider.java index 7197b1c..4398d5b 100644 --- a/src/main/java/com/tcm/MineTale/datagen/ModRecipeProvider.java +++ b/src/main/java/com/tcm/MineTale/datagen/ModRecipeProvider.java @@ -9,13 +9,12 @@ import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider; import net.minecraft.core.HolderLookup; -import net.minecraft.core.registries.Registries; import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.data.recipes.RecipeProvider; import net.minecraft.resources.Identifier; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.CraftingBookCategory; import net.minecraft.world.item.crafting.Ingredient; public class ModRecipeProvider extends FabricRecipeProvider { @@ -45,15 +44,23 @@ protected RecipeProvider createRecipeProvider(HolderLookup.Provider registryLook return new RecipeProvider(registryLookup, exporter) { @Override public void buildRecipes() { - HolderLookup.RegistryLookup itemLookup = registries.lookupOrThrow(Registries.ITEM); - new WorkbenchRecipeBuilder(ModRecipes.CAMPFIRE_TYPE, ModRecipes.CAMPFIRE_SERIALIZER) .input(Ingredient.of(Items.PORKCHOP)) // Note: Slot 1 is optional in our logic, so we just don't add a second input .output(new ItemStack(Items.COOKED_PORKCHOP)) .time(10) // Campfires usually take longer (30 seconds) .unlockedBy("has_porkchop", has(Items.PORKCHOP)) + .category(CraftingBookCategory.MISC) .save(exporter, Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "campfire_pork_cooking")); + + new WorkbenchRecipeBuilder(ModRecipes.FURNACE_TYPE, ModRecipes.FURNACE_SERIALIZER) + .input(Ingredient.of(Items.PORKCHOP)) + // Note: Slot 1 is optional in our logic, so we just don't add a second input + .output(new ItemStack(Items.COOKED_PORKCHOP)) + .time(10) // Campfires usually take longer (30 seconds) + .unlockedBy("has_porkchop", has(Items.PORKCHOP)) + .category(CraftingBookCategory.MISC) + .save(exporter, Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "furnace_pork_cooking")); } }; } diff --git a/src/main/java/com/tcm/MineTale/datagen/builders/WorkbenchRecipeBuilder.java b/src/main/java/com/tcm/MineTale/datagen/builders/WorkbenchRecipeBuilder.java index 5ece69d..395a8d5 100644 --- a/src/main/java/com/tcm/MineTale/datagen/builders/WorkbenchRecipeBuilder.java +++ b/src/main/java/com/tcm/MineTale/datagen/builders/WorkbenchRecipeBuilder.java @@ -25,6 +25,7 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.CraftingBookCategory; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeSerializer; @@ -36,6 +37,7 @@ public class WorkbenchRecipeBuilder implements RecipeBuilder { private final List ingredients = new ArrayList<>(); private final List results = new ArrayList<>(); private final Map> criteria = new LinkedHashMap<>(); + private CraftingBookCategory category = CraftingBookCategory.MISC; private int cookTime = 200; @Nullable private String group; @@ -49,14 +51,16 @@ public class WorkbenchRecipeBuilder implements RecipeBuilder { * @return a MapCodec for WorkbenchRecipe that reads/writes ingredients, results, and cookTime and produces WorkbenchRecipe instances tied to the given type and serializer */ public static final MapCodec CODEC(RecipeType type, RecipeSerializer serializer) { - return RecordCodecBuilder.mapCodec(inst -> inst.group( - Ingredient.CODEC.listOf().fieldOf("ingredients").forGetter(WorkbenchRecipe::ingredients), - ItemStack.STRICT_CODEC.listOf().fieldOf("results").forGetter(WorkbenchRecipe::results), - Codec.INT.optionalFieldOf("cookTime", 200).forGetter(WorkbenchRecipe::cookTime) - ).apply(inst, (ingredients, results, cookTime) -> - new WorkbenchRecipe(ingredients, results, cookTime, type, serializer) - )); -} + return RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC.listOf().fieldOf("ingredients").forGetter(WorkbenchRecipe::ingredients), + ItemStack.STRICT_CODEC.listOf().fieldOf("results").forGetter(WorkbenchRecipe::results), + Codec.INT.optionalFieldOf("cookTime", 200).forGetter(WorkbenchRecipe::cookTime), + // Updated to CraftingBookCategory codec + CraftingBookCategory.CODEC.optionalFieldOf("category", CraftingBookCategory.MISC).forGetter(WorkbenchRecipe::category) + ).apply(inst, (ingredients, results, cookTime, category) -> + new WorkbenchRecipe(ingredients, results, cookTime, type, serializer, category) + )); + } /** * Create a new WorkbenchRecipeBuilder configured for a specific recipe type and its serializer. @@ -91,6 +95,11 @@ public WorkbenchRecipeBuilder input(Ingredient ingredient) { return this; } + public WorkbenchRecipeBuilder category(CraftingBookCategory category) { + this.category = category; + return this; + } + /** * Adds an output item stack to the recipe. * @@ -188,7 +197,8 @@ public void save(RecipeOutput recipeOutput, ResourceKey> resourceKey) List.copyOf(results), cookTime, this.type, - this.serializer + this.serializer, + this.category ); // 4. Accept the recipe into the generator diff --git a/src/main/java/com/tcm/MineTale/item/ModCreativeTab.java b/src/main/java/com/tcm/MineTale/item/ModCreativeTab.java index 4bed274..949e284 100644 --- a/src/main/java/com/tcm/MineTale/item/ModCreativeTab.java +++ b/src/main/java/com/tcm/MineTale/item/ModCreativeTab.java @@ -13,10 +13,11 @@ public class ModCreativeTab { public static final ResourceKey MINETALE_CREATIVE_TAB_KEY = ResourceKey.create(BuiltInRegistries.CREATIVE_MODE_TAB.key(), Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "creative_tab")); public static final CreativeModeTab MINETALE_CREATIVE_TAB = FabricItemGroup.builder() - .icon(() -> new ItemStack(ModBlocks.FURNACE_WORKBENCH_BLOCK)) + .icon(() -> new ItemStack(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1)) .title(Component.translatable("minetale.creative_tab.title")) .displayItems((params, output) -> { - output.accept(ModBlocks.FURNACE_WORKBENCH_BLOCK); + output.accept(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1); + output.accept(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2); output.accept(ModBlocks.CAMPFIRE_WORKBENCH_BLOCK); output.accept(ModBlocks.AMBER_LOG); output.accept(ModBlocks.ASH_LOG); diff --git a/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipe.java b/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipe.java index b4713e7..fccf6df 100644 --- a/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipe.java +++ b/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipe.java @@ -6,12 +6,14 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.tcm.MineTale.registry.ModRecipeDisplay; import net.minecraft.core.HolderLookup; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingBookCategory; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.PlacementInfo; import net.minecraft.world.item.crafting.Recipe; @@ -21,115 +23,13 @@ import net.minecraft.world.item.crafting.display.RecipeDisplay; import net.minecraft.world.level.Level; -// public record WorkbenchRecipe( -// List ingredients, -// List results, -// int cookTime, -// RecipeType recipeType, -// RecipeSerializer recipeSerializer // Added property -// ) implements Recipe { -// public static final int DEFAULT_COOK_TIME = 200; -// public static final int MAX_INPUTS = 2; -// public static final int MAX_OUTPUTS = 4; - -// public WorkbenchRecipe(List ingredients, List results, int cookTime, RecipeType recipeType, RecipeSerializer recipeSerializer) { -// this.ingredients = ingredients; -// this.results = results; -// this.cookTime = cookTime; -// this.recipeType = recipeType; -// this.recipeSerializer = recipeSerializer; -// } - -// public NonNullList getIngredients() { -// NonNullList list = NonNullList.create(); -// list.addAll(this.ingredients); -// return list; -// } - -// @Override -// public boolean matches(RecipeInput input, Level level) { -// if (level.isClientSide()) return false; -// if (ingredients.isEmpty()) return false; - -// // Match slot 0 -// boolean slot0Matches = ingredients.get(0).test(input.getItem(0)); - -// // Match slot 1 if the recipe has a second ingredient -// if (ingredients.size() > 1) { -// return slot0Matches && ingredients.get(1).test(input.getItem(1)); -// } - -// // If recipe only has 1 ingredient, slot 1 must be empty -// return slot0Matches && input.getItem(1).isEmpty(); -// } - -// @Override -// public ItemStack assemble(RecipeInput input, HolderLookup.Provider provider) { -// return results.isEmpty() ? ItemStack.EMPTY : results.get(0).copy(); -// } - -// @Override -// public RecipeSerializer> getSerializer() { -// return this.recipeSerializer; -// } - -// @Override -// public RecipeType> getType() { -// return this.recipeType; -// } - -// @Override -// public PlacementInfo placementInfo() { -// // This tells the recipe book how to place ingredients in your block's slots -// return PlacementInfo.create(this.ingredients); -// } - -// @Override -// public RecipeBookCategory recipeBookCategory() { -// // Return null if you aren't using the vanilla recipe book categories -// return null; -// } - -// @Override -// public List display() { -// return List.of(); -// } - -// // --- Serializer Class --- -// public static class Serializer implements RecipeSerializer { -// private final RecipeType recipeType; -// private final MapCodec codec; -// private final StreamCodec streamCodec; - -// public Serializer(RecipeType recipeType) { -// this.recipeType = recipeType; - -// // Note: We pass 'this' as the serializer into the constructor -// this.codec = RecordCodecBuilder.mapCodec(inst -> inst.group( -// Ingredient.CODEC.listOf().fieldOf("ingredients").forGetter(WorkbenchRecipe::ingredients), -// ItemStack.STRICT_CODEC.listOf().fieldOf("results").forGetter(WorkbenchRecipe::results), -// Codec.INT.optionalFieldOf("cookTime", 200).forGetter(WorkbenchRecipe::cookTime) -// ).apply(inst, (ing, res, time) -> new WorkbenchRecipe(ing, res, time, this.recipeType, this))); - -// this.streamCodec = StreamCodec.composite( -// Ingredient.CONTENTS_STREAM_CODEC.apply(ByteBufCodecs.list()), WorkbenchRecipe::ingredients, -// ItemStack.STREAM_CODEC.apply(ByteBufCodecs.list()), WorkbenchRecipe::results, -// ByteBufCodecs.VAR_INT, WorkbenchRecipe::cookTime, -// (ing, res, time) -> new WorkbenchRecipe(ing, res, time, this.recipeType, this) -// ); -// } - -// @Override public MapCodec codec() { return codec; } -// @Override public StreamCodec streamCodec() { return streamCodec; } -// } -// } - public record WorkbenchRecipe( List ingredients, List results, int cookTime, RecipeType recipeType, - RecipeSerializer recipeSerializer + RecipeSerializer recipeSerializer, + CraftingBookCategory category ) implements Recipe { /** @@ -144,22 +44,28 @@ public record WorkbenchRecipe( */ @Override public boolean matches(WorkbenchRecipeInput input, Level level) { - if (ingredients.isEmpty()) return false; - - // Use .getItem() to get the actual stack from the input - ItemStack stackA = input.inputA(); - ItemStack stackB = input.inputB(); + if (input == null || ingredients.isEmpty()) return false; - // Check if slot 0 matches the first ingredient - boolean slot0Matches = ingredients.get(0).test(stackA); + ItemStack slotA = input.inputA(); + ItemStack slotB = input.inputB(); + Ingredient recipeIngredient = ingredients.get(0); - if (ingredients.size() > 1) { - // Recipe needs two items - return slot0Matches && ingredients.get(1).test(stackB); - } else { - // Recipe only needs one item, so Slot B MUST be empty - return slot0Matches && stackB.isEmpty(); + // If the recipe only has 1 ingredient (like your pork recipe) + if (ingredients.size() == 1) { + // Check if the ingredient matches either slot AND the other slot is empty + boolean matchesA = recipeIngredient.test(slotA) && slotB.isEmpty(); + boolean matchesB = recipeIngredient.test(slotB) && slotA.isEmpty(); + + return matchesA || matchesB; + } + + if (ingredients.size() == 2) { + Ingredient secondIngredient = ingredients.get(1); + return (recipeIngredient.test(slotA) && secondIngredient.test(slotB)) || + (recipeIngredient.test(slotB) && secondIngredient.test(slotA)); } + + return false; } /** @@ -213,8 +119,12 @@ public PlacementInfo placementInfo() { */ @Override public RecipeBookCategory recipeBookCategory() { - // Using null as we are using a custom workbench - return null; + return ModRecipeDisplay.CAMPFIRE_SEARCH; + } + + @Override + public CraftingBookCategory category() { + return this.category; } /** @@ -224,8 +134,9 @@ public RecipeBookCategory recipeBookCategory() { */ @Override public List display() { - // Used for the recipe book UI display - return List.of(); + // Every time the client asks for this recipe's "looks", + // we provide our custom display. + return List.of(new WorkbenchRecipeDisplay(this)); } // --- SERIALIZER --- @@ -247,6 +158,7 @@ public static class Serializer implements RecipeSerializer { public Serializer(RecipeType recipeType) { this.recipeType = recipeType; + // 1. Updated MapCodec to use CraftingBookCategory this.codec = RecordCodecBuilder.mapCodec(inst -> inst.group( Ingredient.CODEC.listOf() .validate(list -> list.size() >= 1 && list.size() <= 2 @@ -258,14 +170,19 @@ public Serializer(RecipeType recipeType) { ? DataResult.success(list) : DataResult.error(() -> "Results must be between 1 and 4")) .fieldOf("results").forGetter(WorkbenchRecipe::results), - Codec.INT.optionalFieldOf("cookTime", 200).forGetter(WorkbenchRecipe::cookTime) - ).apply(inst, (ing, res, time) -> new WorkbenchRecipe(ing, res, time, this.recipeType, this))); + Codec.INT.optionalFieldOf("cookTime", 200).forGetter(WorkbenchRecipe::cookTime), + // Changed CookingBookCategory to CraftingBookCategory + CraftingBookCategory.CODEC.optionalFieldOf("category", CraftingBookCategory.MISC).forGetter(WorkbenchRecipe::category) + ).apply(inst, (ing, res, time, cat) -> new WorkbenchRecipe(ing, res, time, this.recipeType, this, cat))); + // 2. Updated StreamCodec to use CraftingBookCategory this.streamCodec = StreamCodec.composite( Ingredient.CONTENTS_STREAM_CODEC.apply(ByteBufCodecs.list()), WorkbenchRecipe::ingredients, ItemStack.STREAM_CODEC.apply(ByteBufCodecs.list()), WorkbenchRecipe::results, ByteBufCodecs.VAR_INT, WorkbenchRecipe::cookTime, - (ing, res, time) -> new WorkbenchRecipe(ing, res, time, this.recipeType, this) + // Changed CookingBookCategory to CraftingBookCategory + CraftingBookCategory.STREAM_CODEC, WorkbenchRecipe::category, + (ing, res, time, cat) -> new WorkbenchRecipe(ing, res, time, this.recipeType, this, cat) ); } diff --git a/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipeDisplay.java b/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipeDisplay.java new file mode 100644 index 0000000..020a27f --- /dev/null +++ b/src/main/java/com/tcm/MineTale/recipe/WorkbenchRecipeDisplay.java @@ -0,0 +1,54 @@ +package com.tcm.MineTale.recipe; + +import java.util.List; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.tcm.MineTale.registry.ModBlocks; +import com.tcm.MineTale.registry.ModRecipeDisplay; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +public record WorkbenchRecipeDisplay(List ingredients, + SlotDisplay result, + SlotDisplay craftingStation) implements RecipeDisplay { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + SlotDisplay.CODEC.listOf().fieldOf("ingredients").forGetter(WorkbenchRecipeDisplay::ingredients), + SlotDisplay.CODEC.fieldOf("result").forGetter(WorkbenchRecipeDisplay::result), + SlotDisplay.CODEC.fieldOf("crafting_station").forGetter(WorkbenchRecipeDisplay::craftingStation) // Just one fieldOf + ).apply(inst, WorkbenchRecipeDisplay::new)); + + public WorkbenchRecipeDisplay(WorkbenchRecipe recipe) { + this( + recipe.ingredients().stream().map(Ingredient::display).toList(), + new SlotDisplay.ItemStackSlotDisplay(recipe.results().isEmpty() ? ItemStack.EMPTY : recipe.results().get(0)), + new SlotDisplay.ItemStackSlotDisplay(new ItemStack(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1)) + ); + } + + @Override + public List ingredients() { + return this.ingredients; + } + + @Override + public SlotDisplay result() { + return this.result; + } + + @Override + public RecipeDisplay.Type type() { + // We will register this next + return ModRecipeDisplay.WORKBENCH_TYPE; + } + + @Override + public SlotDisplay craftingStation() { + // Replace 'ModBlocks.YOUR_WORKBENCH' with your actual block item + return new SlotDisplay.ItemStackSlotDisplay(new ItemStack(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1)); + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/registry/ModBlockEntities.java b/src/main/java/com/tcm/MineTale/registry/ModBlockEntities.java index 6daa760..fb42df4 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModBlockEntities.java +++ b/src/main/java/com/tcm/MineTale/registry/ModBlockEntities.java @@ -14,25 +14,57 @@ public class ModBlockEntities { + + public static final BlockEntityType CAMPFIRE_WORKBENCH_BE = register( "campfire_workbench_be", CampfireWorkbenchEntity::new, ModBlocks.CAMPFIRE_WORKBENCH_BLOCK ); - public static final BlockEntityType FURNACE_WORKBENCH_BE = register( - "furnace_workbench_be", - FurnaceWorkbenchEntity::new, - ModBlocks.FURNACE_WORKBENCH_BLOCK - ); + // public static final BlockEntityType FURNACE_WORKBENCH_BE_T1 = register( + // "furnace_workbench_be", + // (pos, state) -> new FurnaceWorkbenchEntity("t1", pos, state), + // ModBlocks.FURNACE_WORKBENCH_BLOCK_T1 + // ); + + // public static final BlockEntityType FURNACE_WORKBENCH_BE_T2 = register( + // "furnace_workbench_be", + // (pos, state) -> new FurnaceWorkbenchEntity("t2", pos, state), + // ModBlocks.FURNACE_WORKBENCH_BLOCK_T2 + // ); + + + // Helper method to register AND add to map simultaneously + private static BlockEntityType registerTier(ModTiers.FurnaceTier tier, Block block) { + // If 'block' is null here, the game WILL crash later. + // We can check it now to prove the theory: + if (block == null) { + throw new IllegalStateException("Block for tier " + tier.id() + " is null during BE registration!"); + } + + BlockEntityType type = register( + tier.id() + "_furnace_be", + (pos, state) -> new FurnaceWorkbenchEntity(tier, pos, state, 2, 6), + block + ); + ModTiers.TIER_MAP.put(tier, type); + return type; + } + + public static BlockEntityType FURNACE_WORKBENCH_BE_T1; + public static BlockEntityType FURNACE_WORKBENCH_BE_T2; + /** * Logs a confirmation that the mod's block entity types have been registered. * * Prints "Registered Mod Entities for {modId}" to standard output, where `{modId}` is the mod's identifier. */ public static void initialize() { + FURNACE_WORKBENCH_BE_T1 = registerTier(ModTiers.TIER_1, ModBlocks.FURNACE_WORKBENCH_BLOCK_T1); + FURNACE_WORKBENCH_BE_T2 = registerTier(ModTiers.TIER_2, ModBlocks.FURNACE_WORKBENCH_BLOCK_T2); System.out.println("Registered Mod Entities for " + MineTale.MOD_ID); } diff --git a/src/main/java/com/tcm/MineTale/registry/ModBlocks.java b/src/main/java/com/tcm/MineTale/registry/ModBlocks.java index 438384e..276d8b0 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModBlocks.java +++ b/src/main/java/com/tcm/MineTale/registry/ModBlocks.java @@ -34,9 +34,16 @@ public class ModBlocks { true ); - public static final Block FURNACE_WORKBENCH_BLOCK = register( - "furnace_workbench_block", - FurnaceWorkbench::new, + public static final Block FURNACE_WORKBENCH_BLOCK_T1 = register( + "furnace_workbench_block_t1", + (props) -> new FurnaceWorkbench(props, ModTiers.TIER_1), + BlockBehaviour.Properties.of().sound(SoundType.WOOD), + true + ); + + public static final Block FURNACE_WORKBENCH_BLOCK_T2 = register( + "furnace_workbench_block_t2", + (props) -> new FurnaceWorkbench(props, ModTiers.TIER_2), BlockBehaviour.Properties.of().sound(SoundType.WOOD), true ); @@ -82,7 +89,8 @@ public class ModBlocks { public static void initialize() { ItemGroupEvents.modifyEntriesEvent(CreativeModeTabs.FUNCTIONAL_BLOCKS).register(entries -> { entries.accept(CAMPFIRE_WORKBENCH_BLOCK); - entries.accept(FURNACE_WORKBENCH_BLOCK); + entries.accept(FURNACE_WORKBENCH_BLOCK_T1); + entries.accept(FURNACE_WORKBENCH_BLOCK_T2); }); diff --git a/src/main/java/com/tcm/MineTale/registry/ModRecipeDisplay.java b/src/main/java/com/tcm/MineTale/registry/ModRecipeDisplay.java new file mode 100644 index 0000000..f089993 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/registry/ModRecipeDisplay.java @@ -0,0 +1,44 @@ +package com.tcm.MineTale.registry; + +import com.tcm.MineTale.MineTale; +import com.tcm.MineTale.recipe.WorkbenchRecipeDisplay; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.crafting.RecipeBookCategory; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +public class ModRecipeDisplay { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + SlotDisplay.STREAM_CODEC.apply(ByteBufCodecs.list()), WorkbenchRecipeDisplay::ingredients, + SlotDisplay.STREAM_CODEC, WorkbenchRecipeDisplay::result, + SlotDisplay.STREAM_CODEC, WorkbenchRecipeDisplay::craftingStation, + // Explicitly define the constructor mapping to avoid the Function3 error + (ingredients, result, craftingStation) -> new WorkbenchRecipeDisplay(ingredients, result, craftingStation) + ); + + public static final RecipeDisplay.Type WORKBENCH_TYPE = + new RecipeDisplay.Type<>(WorkbenchRecipeDisplay.CODEC, STREAM_CODEC); + + public static final RecipeBookCategory CAMPFIRE_SEARCH = new RecipeBookCategory(); + + public static void initialize() { + // Register the Display TYPE + Registry.register( + BuiltInRegistries.RECIPE_DISPLAY, + Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "workbench_recipe_display"), + WORKBENCH_TYPE + ); + + // Register the Category + Registry.register( + BuiltInRegistries.RECIPE_BOOK_CATEGORY, + Identifier.fromNamespaceAndPath(MineTale.MOD_ID, "campfire_recipe_book_category"), + CAMPFIRE_SEARCH + ); + } +} diff --git a/src/main/java/com/tcm/MineTale/registry/ModRecipes.java b/src/main/java/com/tcm/MineTale/registry/ModRecipes.java index d52efe9..99f0320 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModRecipes.java +++ b/src/main/java/com/tcm/MineTale/registry/ModRecipes.java @@ -13,8 +13,8 @@ public class ModRecipes { // 1. Define the Types (The "Where") - public static final RecipeType FURNACE_TYPE = createType("furnace_alloying"); - public static final RecipeType CAMPFIRE_TYPE = createType("campfire_alloying"); + public static final RecipeType FURNACE_TYPE = createType("furnace_recipe_type"); + public static final RecipeType CAMPFIRE_TYPE = createType("campfire_recipe_type"); // 2. Define the Serializers (The "How") // We pass the specific Type into the Serializer's constructor @@ -24,15 +24,12 @@ public class ModRecipes { public static final RecipeSerializer CAMPFIRE_SERIALIZER = new WorkbenchRecipe.Serializer(CAMPFIRE_TYPE); - /** - * Registers the furnace and campfire alloying recipe types and their serializers into the built-in registries. - */ public static void initialize() { // Register the Furnace-flavored version - register("furnace_alloying", FURNACE_TYPE, FURNACE_SERIALIZER); + register(FURNACE_TYPE.toString(), FURNACE_TYPE, FURNACE_SERIALIZER); // Register the Campfire-flavored version - register("campfire_alloying", CAMPFIRE_TYPE, CAMPFIRE_SERIALIZER); + register(CAMPFIRE_TYPE.toString(), CAMPFIRE_TYPE, CAMPFIRE_SERIALIZER); } /** diff --git a/src/main/java/com/tcm/MineTale/registry/ModTiers.java b/src/main/java/com/tcm/MineTale/registry/ModTiers.java new file mode 100644 index 0000000..7a34861 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/registry/ModTiers.java @@ -0,0 +1,28 @@ +package com.tcm.MineTale.registry; + +import java.util.HashMap; +import java.util.Map; + +import com.tcm.MineTale.block.workbenches.entity.FurnaceWorkbenchEntity; + +import net.minecraft.world.level.block.entity.BlockEntityType; + +public class ModTiers { + public record FurnaceTier( + int id, + int cookTime, + double scanRadius + ) {} + + public static final Map> TIER_MAP = new HashMap<>(); + + public static FurnaceTier getTierFromInt(int id) { + return TIER_MAP.keySet().stream() + .filter(t -> t.id() == id) + .findFirst() + .orElse(TIER_1); + } + + public static final FurnaceTier TIER_1 = new FurnaceTier(1, 200, 8.0); + public static final FurnaceTier TIER_2 = new FurnaceTier(2, 100, 8.0); +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/util/Constants.java b/src/main/java/com/tcm/MineTale/util/Constants.java index 4f63c30..2b2baea 100644 --- a/src/main/java/com/tcm/MineTale/util/Constants.java +++ b/src/main/java/com/tcm/MineTale/util/Constants.java @@ -16,13 +16,5 @@ private Constants() { // Input & Fuel public static final int FUEL_SLOT = 0; - public static final int INPUT_1 = 1; - public static final int INPUT_2 = 2; - - // Output range - public static final int OUTPUT_START = 3; - public static final int OUTPUT_END = 6; - - // Derived constant for convenience - public static final int TOTAL_SLOTS = 7; + public static final int INPUT_START = 1; } \ No newline at end of file diff --git a/src/main/resources/data/minetale/recipe_book_category/campfire_recipe_book_category.json b/src/main/resources/data/minetale/recipe_book_category/campfire_recipe_book_category.json new file mode 100644 index 0000000..cb3f258 --- /dev/null +++ b/src/main/resources/data/minetale/recipe_book_category/campfire_recipe_book_category.json @@ -0,0 +1,6 @@ +{ + "category": "minetale:campfire_recipe_book_category", + "recipe_types": [ + "minetale:campfire_recipe_type" + ] +} \ No newline at end of file