Skip to content

Commit cd2c623

Browse files
authored
item/recipe: Implement dynamic recipes system with decorated pot support (#1130)
1 parent e63f9d5 commit cd2c623

File tree

5 files changed

+221
-2
lines changed

5 files changed

+221
-2
lines changed

server/item/recipe/dynamic.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package recipe
2+
3+
import (
4+
"github.com/df-mc/dragonfly/server/item"
5+
"github.com/df-mc/dragonfly/server/world"
6+
)
7+
8+
// DecoratedPotRecipe is a dynamic recipe for crafting decorated pots. The output depends on which
9+
// pottery sherds or bricks are used in the crafting grid.
10+
type DecoratedPotRecipe struct {
11+
block string
12+
}
13+
14+
// NewDecoratedPotRecipe creates a new decorated pot recipe.
15+
func NewDecoratedPotRecipe() DecoratedPotRecipe {
16+
return DecoratedPotRecipe{block: "crafting_table"}
17+
}
18+
19+
// potDecoration is a local interface to check if an item can be used as a pot decoration
20+
// without importing the block package (which would create an import cycle).
21+
type potDecoration interface {
22+
world.Item
23+
PotDecoration() bool
24+
}
25+
26+
// Match checks if the given input items match the decorated pot recipe pattern.
27+
// The pattern requires exactly 4 PotDecoration items (bricks or pottery sherds) in a diamond/plus shape:
28+
// - Slot 1 (top centre)
29+
// - Slot 3 (middle left)
30+
// - Slot 5 (middle right)
31+
// - Slot 7 (bottom centre)
32+
// All other slots must be empty.
33+
func (r DecoratedPotRecipe) Match(input []Item) (output []item.Stack, ok bool) {
34+
// For a 3x3 crafting grid, we need exactly 9 slots
35+
if len(input) != 9 {
36+
return nil, false
37+
}
38+
39+
// Define the slots for the diamond pattern (0-indexed)
40+
// Layout: 0 1 2
41+
// 3 4 5
42+
// 6 7 8
43+
// We need items at: 1 (top), 3 (left), 5 (right), 7 (bottom)
44+
// Odd indices should have items, even indices should be empty
45+
46+
decorations := [4]world.Item{}
47+
decorationIndex := 0
48+
for i := range input {
49+
it := input[i]
50+
if i%2 == 0 {
51+
// Even slots (0, 2, 4, 6, 8) should be empty
52+
if !it.Empty() {
53+
return nil, false
54+
}
55+
} else {
56+
// Odd slots (1, 3, 5, 7) should have items
57+
if it.Empty() {
58+
return nil, false
59+
}
60+
61+
// Extract the actual item from the Item interface
62+
var actualItem item.Stack
63+
if v, ok := it.(item.Stack); ok {
64+
actualItem = v
65+
} else {
66+
// ItemTag or other types are not valid for decorated pots
67+
return nil, false
68+
}
69+
70+
// Check if the item implements PotDecoration
71+
decoration, ok := actualItem.Item().(potDecoration)
72+
if !ok {
73+
return nil, false
74+
}
75+
decorations[decorationIndex] = decoration
76+
decorationIndex++
77+
}
78+
}
79+
80+
// Create the decorated pot by encoding the decorations into NBT
81+
// We'll use world.BlockByName to get the DecoratedPot block and set its decorations
82+
// The decorations are ordered: [top, left, right, bottom] in the crafting grid
83+
// For the pot NBT: [back, left, front, right] based on facing direction
84+
85+
// Get a decorated pot block instance
86+
pot, ok := world.BlockByName("minecraft:decorated_pot", map[string]any{"direction": int32(2)})
87+
if !ok {
88+
return nil, false
89+
}
90+
91+
// The pot will be decoded with the decorations through NBT when placed
92+
// For now, we'll create a pot with the decorations in the correct order
93+
// DecoratedPot.DecodeNBT expects sherds in order: [back, left, front, right]
94+
sherds := []any{}
95+
// Order: top -> back, left -> left, bottom -> front, right -> right
96+
for _, idx := range []int{0, 1, 3, 2} { // top, left, bottom, right
97+
name, _ := decorations[idx].EncodeItem()
98+
sherds = append(sherds, name)
99+
}
100+
101+
// Decode the pot with the sherds NBT data using type assertion
102+
if nbtDecoder, ok := pot.(interface {
103+
DecodeNBT(map[string]any) any
104+
}); ok {
105+
decodedPot := nbtDecoder.DecodeNBT(map[string]any{
106+
"id": "DecoratedPot",
107+
"sherds": sherds,
108+
})
109+
if potItem, ok := decodedPot.(world.Item); ok {
110+
return []item.Stack{item.NewStack(potItem, 1)}, true
111+
}
112+
}
113+
114+
return nil, false
115+
}
116+
117+
// Block returns the block used to craft this recipe.
118+
func (r DecoratedPotRecipe) Block() string {
119+
return r.block
120+
}

server/item/recipe/recipe.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ type Recipe interface {
1818
Priority() uint32
1919
}
2020

21+
// DynamicRecipe represents a recipe whose output depends on the specific items used in crafting.
22+
// These recipes are not sent to the client and are validated server-side.
23+
type DynamicRecipe interface {
24+
// Match checks if the given input items match this dynamic recipe pattern.
25+
// It returns true if the pattern matches, along with the computed output items.
26+
Match(input []Item) (output []item.Stack, ok bool)
27+
// Block returns the block that is used to craft the recipe.
28+
Block() string
29+
}
30+
2131
// Shapeless is a recipe that has no particular shape.
2232
type Shapeless struct {
2333
recipe

server/item/recipe/register.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
// recipes is a list of each recipe.
1414
var (
1515
recipes []Recipe
16+
// dynamicRecipes is a list of each dynamic recipe.
17+
dynamicRecipes []DynamicRecipe
1618
// index maps an input hash to output stacks for each PotionContainerChange and Potion recipe.
1719
index = make(map[string]map[string]Recipe)
1820
// reagent maps the item name and an item.Stack.
@@ -24,6 +26,11 @@ func Recipes() []Recipe {
2426
return slices.Clone(recipes)
2527
}
2628

29+
// DynamicRecipes returns each dynamic recipe in a slice.
30+
func DynamicRecipes() []DynamicRecipe {
31+
return slices.Clone(dynamicRecipes)
32+
}
33+
2734
// Register registers a new recipe.
2835
func Register(recipe Recipe) {
2936
recipes = append(recipes, recipe)
@@ -115,3 +122,9 @@ func ValidBrewingReagent(i world.Item) bool {
115122
_, exists := reagent[name]
116123
return exists
117124
}
125+
126+
// RegisterDynamic registers a new dynamic recipe. Dynamic recipes are not sent to the client
127+
// and are validated server-side.
128+
func RegisterDynamic(recipe DynamicRecipe) {
129+
dynamicRecipes = append(dynamicRecipes, recipe)
130+
}

server/item/recipe/vanilla.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,7 @@ func registerVanilla() {
204204
block: "brewing_stand",
205205
}})
206206
}
207+
208+
// Register dynamic recipes
209+
RegisterDynamic(NewDecoratedPotRecipe())
207210
}

server/session/handler_crafting.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
1717
craft, ok := s.recipes[a.RecipeNetworkID]
1818
if !ok {
19-
return fmt.Errorf("recipe with network id %v does not exist", a.RecipeNetworkID)
19+
// Try dynamic recipes if no static recipe matches
20+
return h.tryDynamicCraft(s, tx, int(a.NumberOfCrafts))
2021
}
2122
_, shaped := craft.(recipe.Shaped)
2223
_, shapeless := craft.(recipe.Shapeless)
@@ -70,7 +71,8 @@ func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackReques
7071
func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
7172
craft, ok := s.recipes[a.RecipeNetworkID]
7273
if !ok {
73-
return fmt.Errorf("recipe with network id %v does not exist", a.RecipeNetworkID)
74+
// Try dynamic recipes if no static recipe matches
75+
return h.tryDynamicCraft(s, tx, int(a.TimesCrafted))
7476
}
7577
_, shaped := craft.(recipe.Shaped)
7678
_, shapeless := craft.(recipe.Shapeless)
@@ -237,3 +239,74 @@ func grow(i recipe.Item, count int) recipe.Item {
237239
}
238240
panic(fmt.Errorf("unexpected recipe item %T", i))
239241
}
242+
243+
// tryDynamicCraft attempts to match the items in the crafting grid with any registered dynamic recipes.
244+
func (h *ItemStackRequestHandler) tryDynamicCraft(s *Session, tx *world.Tx, timesCrafted int) error {
245+
if timesCrafted < 1 {
246+
return fmt.Errorf("times crafted must be at least 1")
247+
}
248+
249+
size := s.craftingSize()
250+
offset := s.craftingOffset()
251+
252+
// Collect all items from the crafting grid
253+
input := make([]recipe.Item, size)
254+
for i := uint32(0); i < size; i++ {
255+
slot := offset + i
256+
it, _ := s.ui.Item(int(slot))
257+
if it.Empty() {
258+
input[i] = item.Stack{}
259+
} else {
260+
input[i] = it
261+
}
262+
}
263+
264+
// Try to match with any dynamic recipe
265+
for _, dynamicRecipe := range recipe.DynamicRecipes() {
266+
if dynamicRecipe.Block() != "crafting_table" {
267+
continue
268+
}
269+
270+
output, ok := dynamicRecipe.Match(input)
271+
if !ok {
272+
continue
273+
}
274+
275+
// Found a matching dynamic recipe! Now validate ingredient counts and consume the items
276+
// For dynamic recipes, we consume all non-empty slots, but we need to ensure each slot
277+
// has enough items to craft timesCrafted times.
278+
minStackCount := math.MaxInt
279+
for i := uint32(0); i < size; i++ {
280+
slot := offset + i
281+
it, _ := s.ui.Item(int(slot))
282+
if !it.Empty() {
283+
if it.Count() < minStackCount {
284+
minStackCount = it.Count()
285+
}
286+
}
287+
}
288+
289+
// Cap timesCrafted to the minimum available stack count to prevent item duplication
290+
if minStackCount < timesCrafted {
291+
timesCrafted = minStackCount
292+
}
293+
294+
// Now consume the validated amount from each non-empty slot
295+
for i := uint32(0); i < size; i++ {
296+
slot := offset + i
297+
it, _ := s.ui.Item(int(slot))
298+
if !it.Empty() {
299+
// Consume one item from this slot per craft
300+
st := it.Grow(-1 * timesCrafted)
301+
h.setItemInSlot(protocol.StackRequestSlotInfo{
302+
Container: protocol.FullContainerName{ContainerID: protocol.ContainerCraftingInput},
303+
Slot: byte(slot),
304+
}, st, s, tx)
305+
}
306+
}
307+
308+
return h.createResults(s, tx, repeatStacks(output, timesCrafted)...)
309+
}
310+
311+
return fmt.Errorf("no matching recipe found for crafting grid")
312+
}

0 commit comments

Comments
 (0)