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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions server/item/recipe/dynamic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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 centre)
// - Slot 3 (middle left)
// - Slot 5 (middle right)
// - 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
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)
// Odd indices should have items, even indices should be empty

decorations := [4]world.Item{}
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++
}
}

// 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
}
10 changes: 10 additions & 0 deletions server/item/recipe/recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions server/item/recipe/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions server/item/recipe/vanilla.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,7 @@ func registerVanilla() {
block: "brewing_stand",
}})
}

// Register dynamic recipes
RegisterDynamic(NewDecoratedPotRecipe())
}
77 changes: 75 additions & 2 deletions server/session/handler_crafting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}