From 31f2f999a7d4c8202ffe9ba3fbba421b1dc5c222 Mon Sep 17 00:00:00 2001 From: manab-pr Date: Sun, 9 Nov 2025 08:32:05 +0530 Subject: [PATCH 1/3] Support for dynamic crafting recipes --- server/item/recipe/dynamic.go | 122 +++++++++++++++++++++++++++++ server/item/recipe/recipe.go | 10 +++ server/item/recipe/register.go | 13 +++ server/item/recipe/vanilla.go | 3 + server/session/handler_crafting.go | 77 +++++++++++++++++- 5 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 server/item/recipe/dynamic.go diff --git a/server/item/recipe/dynamic.go b/server/item/recipe/dynamic.go new file mode 100644 index 000000000..a2011164e --- /dev/null +++ b/server/item/recipe/dynamic.go @@ -0,0 +1,122 @@ +package recipe + +import ( + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// DecoratedPotRecipe is a dynamic recipe for crafting decorated pots. The output depends on which +// pottery sherds or bricks are used in the crafting grid. +type DecoratedPotRecipe struct { + block string +} + +// NewDecoratedPotRecipe creates a new decorated pot recipe. +func NewDecoratedPotRecipe() DecoratedPotRecipe { + return DecoratedPotRecipe{block: "crafting_table"} +} + +// potDecoration is a local interface to check if an item can be used as a pot decoration +// without importing the block package (which would create an import cycle). +type potDecoration interface { + world.Item + PotDecoration() bool +} + +// Match checks if the given input items match the decorated pot recipe pattern. +// The pattern requires exactly 4 PotDecoration items (bricks or pottery sherds) in a diamond/plus shape: +// - Slot 1 (top center) +// - Slot 3 (middle left) +// - Slot 5 (middle right) +// - Slot 7 (bottom center) +// All other slots must be empty. +func (r DecoratedPotRecipe) Match(input []Item) (output []item.Stack, ok bool) { + // For a 3x3 crafting grid, we need exactly 9 slots + if len(input) != 9 { + return nil, false + } + + // Define the slots for the diamond pattern (0-indexed) + // Layout: 0 1 2 + // 3 4 5 + // 6 7 8 + // We need items at: 1 (top), 3 (left), 5 (right), 7 (bottom) + requiredSlots := []int{1, 3, 5, 7} + emptySlots := []int{0, 2, 4, 6, 8} + + // Check that empty slots are actually empty + for _, slot := range emptySlots { + if !input[slot].Empty() { + return nil, false + } + } + + // Collect decorations from the required slots + decorations := [4]world.Item{} + for i, slot := range requiredSlots { + it := input[slot] + if it.Empty() { + return nil, false + } + + // Extract the actual item from the Item interface + var actualItem item.Stack + switch v := it.(type) { + case item.Stack: + actualItem = v + case ItemTag: + // ItemTag is not valid for decorated pots + return nil, false + default: + return nil, false + } + + // Check if the item implements PotDecoration + decoration, ok := actualItem.Item().(potDecoration) + if !ok { + return nil, false + } + decorations[i] = decoration + } + + // Create the decorated pot by encoding the decorations into NBT + // We'll use world.BlockByName to get the DecoratedPot block and set its decorations + // The decorations are ordered: [top, left, right, bottom] in the crafting grid + // For the pot NBT: [back, left, front, right] based on facing direction + + // Get a decorated pot block instance + pot, ok := world.BlockByName("minecraft:decorated_pot", map[string]any{"direction": int32(2)}) + if !ok { + return nil, false + } + + // The pot will be decoded with the decorations through NBT when placed + // For now, we'll create a pot with the decorations in the correct order + // DecoratedPot.DecodeNBT expects sherds in order: [back, left, front, right] + sherds := []any{} + // Order: top -> back, left -> left, bottom -> front, right -> right + for _, idx := range []int{0, 1, 3, 2} { // top, left, bottom, right + name, _ := decorations[idx].EncodeItem() + sherds = append(sherds, name) + } + + // Decode the pot with the sherds NBT data using type assertion + if nbtDecoder, ok := pot.(interface { + DecodeNBT(map[string]any) any + }); ok { + decodedPot := nbtDecoder.DecodeNBT(map[string]any{ + "id": "DecoratedPot", + "sherds": sherds, + }) + if potItem, ok := decodedPot.(world.Item); ok { + return []item.Stack{item.NewStack(potItem, 1)}, true + } + } + + return nil, false +} + +// Block returns the block used to craft this recipe. +func (r DecoratedPotRecipe) Block() string { + return r.block +} diff --git a/server/item/recipe/recipe.go b/server/item/recipe/recipe.go index 3332b1a96..4ba17bbd2 100644 --- a/server/item/recipe/recipe.go +++ b/server/item/recipe/recipe.go @@ -18,6 +18,16 @@ type Recipe interface { Priority() uint32 } +// DynamicRecipe represents a recipe whose output depends on the specific items used in crafting. +// These recipes are not sent to the client and are validated server-side. +type DynamicRecipe interface { + // Match checks if the given input items match this dynamic recipe pattern. + // It returns true if the pattern matches, along with the computed output items. + Match(input []Item) (output []item.Stack, ok bool) + // Block returns the block that is used to craft the recipe. + Block() string +} + // Shapeless is a recipe that has no particular shape. type Shapeless struct { recipe diff --git a/server/item/recipe/register.go b/server/item/recipe/register.go index 3cebd850a..f8fe0bc08 100644 --- a/server/item/recipe/register.go +++ b/server/item/recipe/register.go @@ -13,6 +13,8 @@ import ( // recipes is a list of each recipe. var ( recipes []Recipe + // dynamicRecipes is a list of each dynamic recipe. + dynamicRecipes []DynamicRecipe // index maps an input hash to output stacks for each PotionContainerChange and Potion recipe. index = make(map[string]map[string]Recipe) // reagent maps the item name and an item.Stack. @@ -24,6 +26,11 @@ func Recipes() []Recipe { return slices.Clone(recipes) } +// DynamicRecipes returns each dynamic recipe in a slice. +func DynamicRecipes() []DynamicRecipe { + return slices.Clone(dynamicRecipes) +} + // Register registers a new recipe. func Register(recipe Recipe) { recipes = append(recipes, recipe) @@ -115,3 +122,9 @@ func ValidBrewingReagent(i world.Item) bool { _, exists := reagent[name] return exists } + +// RegisterDynamic registers a new dynamic recipe. Dynamic recipes are not sent to the client +// and are validated server-side. +func RegisterDynamic(recipe DynamicRecipe) { + dynamicRecipes = append(dynamicRecipes, recipe) +} diff --git a/server/item/recipe/vanilla.go b/server/item/recipe/vanilla.go index 33121f5eb..2df8f9830 100644 --- a/server/item/recipe/vanilla.go +++ b/server/item/recipe/vanilla.go @@ -204,4 +204,7 @@ func registerVanilla() { block: "brewing_stand", }}) } + + // Register dynamic recipes + RegisterDynamic(NewDecoratedPotRecipe()) } diff --git a/server/session/handler_crafting.go b/server/session/handler_crafting.go index 64a4d2b08..ae90c902f 100644 --- a/server/session/handler_crafting.go +++ b/server/session/handler_crafting.go @@ -16,7 +16,8 @@ import ( func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx) error { craft, ok := s.recipes[a.RecipeNetworkID] if !ok { - return fmt.Errorf("recipe with network id %v does not exist", a.RecipeNetworkID) + // Try dynamic recipes if no static recipe matches + return h.tryDynamicCraft(s, tx, int(a.NumberOfCrafts)) } _, shaped := craft.(recipe.Shaped) _, shapeless := craft.(recipe.Shapeless) @@ -70,7 +71,8 @@ func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackReques func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeStackRequestAction, s *Session, tx *world.Tx) error { craft, ok := s.recipes[a.RecipeNetworkID] if !ok { - return fmt.Errorf("recipe with network id %v does not exist", a.RecipeNetworkID) + // Try dynamic recipes if no static recipe matches + return h.tryDynamicCraft(s, tx, int(a.TimesCrafted)) } _, shaped := craft.(recipe.Shaped) _, shapeless := craft.(recipe.Shapeless) @@ -237,3 +239,74 @@ func grow(i recipe.Item, count int) recipe.Item { } panic(fmt.Errorf("unexpected recipe item %T", i)) } + +// tryDynamicCraft attempts to match the items in the crafting grid with any registered dynamic recipes. +func (h *ItemStackRequestHandler) tryDynamicCraft(s *Session, tx *world.Tx, timesCrafted int) error { + if timesCrafted < 1 { + return fmt.Errorf("times crafted must be at least 1") + } + + size := s.craftingSize() + offset := s.craftingOffset() + + // Collect all items from the crafting grid + input := make([]recipe.Item, size) + for i := uint32(0); i < size; i++ { + slot := offset + i + it, _ := s.ui.Item(int(slot)) + if it.Empty() { + input[i] = item.Stack{} + } else { + input[i] = it + } + } + + // Try to match with any dynamic recipe + for _, dynamicRecipe := range recipe.DynamicRecipes() { + if dynamicRecipe.Block() != "crafting_table" { + continue + } + + output, ok := dynamicRecipe.Match(input) + if !ok { + continue + } + + // Found a matching dynamic recipe! Now validate ingredient counts and consume the items + // For dynamic recipes, we consume all non-empty slots, but we need to ensure each slot + // has enough items to craft timesCrafted times. + minStackCount := math.MaxInt + for i := uint32(0); i < size; i++ { + slot := offset + i + it, _ := s.ui.Item(int(slot)) + if !it.Empty() { + if it.Count() < minStackCount { + minStackCount = it.Count() + } + } + } + + // Cap timesCrafted to the minimum available stack count to prevent item duplication + if minStackCount < timesCrafted { + timesCrafted = minStackCount + } + + // Now consume the validated amount from each non-empty slot + for i := uint32(0); i < size; i++ { + slot := offset + i + it, _ := s.ui.Item(int(slot)) + if !it.Empty() { + // Consume one item from this slot per craft + st := it.Grow(-1 * timesCrafted) + h.setItemInSlot(protocol.StackRequestSlotInfo{ + Container: protocol.FullContainerName{ContainerID: protocol.ContainerCraftingInput}, + Slot: byte(slot), + }, st, s, tx) + } + } + + return h.createResults(s, tx, repeatStacks(output, timesCrafted)...) + } + + return fmt.Errorf("no matching recipe found for crafting grid") +} From b20cb0a61f098f83f6076e26ebd89e22b343754d Mon Sep 17 00:00:00 2001 From: manab-pr Date: Sun, 9 Nov 2025 08:59:09 +0530 Subject: [PATCH 2/3] Support for dynamic crafting recipes: corrected spelling --- server/item/recipe/dynamic.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/item/recipe/dynamic.go b/server/item/recipe/dynamic.go index a2011164e..da4e09572 100644 --- a/server/item/recipe/dynamic.go +++ b/server/item/recipe/dynamic.go @@ -25,10 +25,10 @@ type potDecoration interface { // Match checks if the given input items match the decorated pot recipe pattern. // The pattern requires exactly 4 PotDecoration items (bricks or pottery sherds) in a diamond/plus shape: -// - Slot 1 (top center) +// - Slot 1 (top centre) // - Slot 3 (middle left) // - Slot 5 (middle right) -// - Slot 7 (bottom center) +// - Slot 7 (bottom centre) // All other slots must be empty. func (r DecoratedPotRecipe) Match(input []Item) (output []item.Stack, ok bool) { // For a 3x3 crafting grid, we need exactly 9 slots From fc0916a7b5c2a30294522fe67096c5c03a7bc2a1 Mon Sep 17 00:00:00 2001 From: manab-pr Date: Sat, 15 Nov 2025 18:14:30 +0530 Subject: [PATCH 3/3] fix : as suggested , switch case is removed and odd even logic used --- server/item/recipe/dynamic.go | 64 +++++++++++++++++------------------ 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/server/item/recipe/dynamic.go b/server/item/recipe/dynamic.go index da4e09572..02f6531df 100644 --- a/server/item/recipe/dynamic.go +++ b/server/item/recipe/dynamic.go @@ -41,42 +41,40 @@ func (r DecoratedPotRecipe) Match(input []Item) (output []item.Stack, ok bool) { // 3 4 5 // 6 7 8 // We need items at: 1 (top), 3 (left), 5 (right), 7 (bottom) - requiredSlots := []int{1, 3, 5, 7} - emptySlots := []int{0, 2, 4, 6, 8} + // Odd indices should have items, even indices should be empty - // Check that empty slots are actually empty - for _, slot := range emptySlots { - if !input[slot].Empty() { - return nil, false - } - } - - // Collect decorations from the required slots decorations := [4]world.Item{} - for i, slot := range requiredSlots { - it := input[slot] - if it.Empty() { - return nil, false - } - - // Extract the actual item from the Item interface - var actualItem item.Stack - switch v := it.(type) { - case item.Stack: - actualItem = v - case ItemTag: - // ItemTag is not valid for decorated pots - return nil, false - default: - return nil, false - } - - // Check if the item implements PotDecoration - decoration, ok := actualItem.Item().(potDecoration) - if !ok { - return nil, false + decorationIndex := 0 + for i := range input { + it := input[i] + if i%2 == 0 { + // Even slots (0, 2, 4, 6, 8) should be empty + if !it.Empty() { + return nil, false + } + } else { + // Odd slots (1, 3, 5, 7) should have items + if it.Empty() { + return nil, false + } + + // Extract the actual item from the Item interface + var actualItem item.Stack + if v, ok := it.(item.Stack); ok { + actualItem = v + } else { + // ItemTag or other types are not valid for decorated pots + return nil, false + } + + // Check if the item implements PotDecoration + decoration, ok := actualItem.Item().(potDecoration) + if !ok { + return nil, false + } + decorations[decorationIndex] = decoration + decorationIndex++ } - decorations[i] = decoration } // Create the decorated pot by encoding the decorations into NBT