Files
mc/server/session/handler_crafting.go
T
TarnaWijaya 26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
up3
2026-07-09 08:33:57 +08:00

313 lines
9.5 KiB
Go

package session
import (
"fmt"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/creative"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/item/recipe"
"github.com/df-mc/dragonfly/server/world"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"math"
"slices"
)
// handleCraft handles the CraftRecipe request action.
func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
craft, ok := s.recipes[a.RecipeNetworkID]
if !ok {
// Try dynamic recipes if no static recipe matches
return h.tryDynamicCraft(s, tx, int(a.NumberOfCrafts))
}
_, shaped := craft.(recipe.Shaped)
_, shapeless := craft.(recipe.Shapeless)
if !shaped && !shapeless {
return fmt.Errorf("recipe with network id %v is not a shaped or shapeless recipe", a.RecipeNetworkID)
}
if craft.Block() != "crafting_table" {
return fmt.Errorf("recipe with network id %v is not a crafting table recipe", a.RecipeNetworkID)
}
timesCrafted := int(a.NumberOfCrafts)
if timesCrafted < 1 {
return fmt.Errorf("times crafted must be at least 1")
}
size := s.craftingSize()
offset := s.craftingOffset()
consumed := make([]bool, size)
for _, expected := range craft.Input() {
var processed bool
for slot := offset; slot < offset+size; slot++ {
if consumed[slot-offset] {
// We've already consumed this slot, skip it.
continue
}
has, _ := s.ui.Item(int(slot))
if has.Empty() != expected.Empty() || has.Count() < expected.Count()*timesCrafted {
// We can't process this item, as it's not a part of the recipe.
continue
}
if !matchingStacks(has, expected) {
// Not the same item.
continue
}
processed, consumed[slot-offset] = true, true
st := has.Grow(-expected.Count() * timesCrafted)
h.setItemInSlot(protocol.StackRequestSlotInfo{
Container: protocol.FullContainerName{ContainerID: protocol.ContainerCraftingInput},
Slot: byte(slot),
}, st, s, tx)
break
}
if !processed {
return fmt.Errorf("recipe %v: could not consume expected item: %v", a.RecipeNetworkID, expected)
}
}
return h.createResults(s, tx, repeatStacks(craft.Output(), timesCrafted)...)
}
// handleAutoCraft handles the AutoCraftRecipe request action.
func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
craft, ok := s.recipes[a.RecipeNetworkID]
if !ok {
// Try dynamic recipes if no static recipe matches
return h.tryDynamicCraft(s, tx, int(a.TimesCrafted))
}
_, shaped := craft.(recipe.Shaped)
_, shapeless := craft.(recipe.Shapeless)
if !shaped && !shapeless {
return fmt.Errorf("recipe with network id %v is not a shaped or shapeless recipe", a.RecipeNetworkID)
}
if craft.Block() != "crafting_table" {
return fmt.Errorf("recipe with network id %v is not a crafting table recipe", a.RecipeNetworkID)
}
timesCrafted := int(a.TimesCrafted)
if timesCrafted < 1 {
return fmt.Errorf("times crafted must be at least 1")
}
flattenedInputs := make([]recipe.Item, 0, len(craft.Input()))
for _, i := range craft.Input() {
if i.Empty() {
// We don't actually need this item - it's empty, so avoid putting it in our flattened inputs.
continue
}
if ind := slices.IndexFunc(flattenedInputs, func(it recipe.Item) bool {
return matchingStacks(it, i)
}); ind >= 0 {
flattenedInputs[ind] = grow(i, flattenedInputs[ind].Count())
continue
}
flattenedInputs = append(flattenedInputs, i)
}
for _, expected := range flattenedInputs {
remaining := expected.Count() * timesCrafted
for id, inv := range map[byte]*inventory.Inventory{
protocol.ContainerCraftingInput: s.ui,
protocol.ContainerCombinedHotBarAndInventory: s.inv,
} {
for slot, has := range inv.Slots() {
if has.Empty() {
// We don't have this item, skip it.
continue
}
if !matchingStacks(has, expected) {
// Not the same item.
continue
}
removal := has.Count()
if remaining < removal {
removal = remaining
}
remaining -= removal
has = has.Grow(-removal)
h.setItemInSlot(protocol.StackRequestSlotInfo{
Container: protocol.FullContainerName{ContainerID: id},
Slot: byte(slot),
}, has, s, tx)
if remaining == 0 {
// Consumed this item, so go to the next one.
break
}
}
if remaining == 0 {
// Consumed this item, so go to the next one.
break
}
}
if remaining != 0 {
return fmt.Errorf("recipe %v: could not consume expected item: %v", a.RecipeNetworkID, expected)
}
}
return h.createResults(s, tx, repeatStacks(craft.Output(), timesCrafted)...)
}
// handleCreativeCraft handles the CreativeCraft request action.
func (h *ItemStackRequestHandler) handleCreativeCraft(a *protocol.CraftCreativeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
if !c.GameMode().CreativeInventory() {
return fmt.Errorf("can only craft creative items in gamemode creative/spectator")
}
index := a.CreativeItemNetworkID - 1
if int(index) >= len(creative.Items()) {
return fmt.Errorf("creative item with network ID %v does not exist", index)
}
it := creative.Items()[index].Stack
it = it.Grow(it.MaxCount() - 1)
return h.createResults(s, tx, it)
}
// craftingSize gets the crafting size based on the opened container ID.
func (s *Session) craftingSize() uint32 {
if s.openedContainerID.Load() == 1 {
return craftingGridSizeLarge
}
return craftingGridSizeSmall
}
// craftingOffset gets the crafting offset based on the opened container ID.
func (s *Session) craftingOffset() uint32 {
if s.openedContainerID.Load() == 1 {
return craftingGridLargeOffset
}
return craftingGridSmallOffset
}
// matchingStacks returns true if the two stacks are the same in a crafting scenario.
func matchingStacks(has, expected recipe.Item) bool {
switch expected := expected.(type) {
case item.Stack:
switch has := has.(type) {
case recipe.ItemTag:
name, _ := expected.Item().EncodeItem()
return has.Contains(name)
case item.Stack:
_, variants := expected.Value("variants")
if !variants {
return has.Comparable(expected)
}
nameOne, _ := has.Item().EncodeItem()
nameTwo, _ := expected.Item().EncodeItem()
return nameOne == nameTwo
}
panic(fmt.Errorf("client has unexpected recipe item %T", has))
case recipe.ItemTag:
switch has := has.(type) {
case item.Stack:
name, _ := has.Item().EncodeItem()
return expected.Contains(name)
case recipe.ItemTag:
return has.Tag() == expected.Tag()
}
panic(fmt.Errorf("client has unexpected recipe item %T", has))
}
panic(fmt.Errorf("tried to match with unexpected recipe item %T", expected))
}
// repeatStacks multiplies the count of all item stacks provided by the number of repetitions provided. Item
// stacks where the new count would exceed the item's max count are split into multiple item stacks.
func repeatStacks(items []item.Stack, repetitions int) []item.Stack {
output := make([]item.Stack, 0, len(items))
for _, o := range items {
count, maxCount := o.Count(), o.MaxCount()
total := count * repetitions
stacks := int(math.Ceil(float64(total) / float64(maxCount)))
for i := 0; i < stacks; i++ {
inc := min(total, maxCount)
total -= inc
output = append(output, o.Grow(inc-count))
}
}
return output
}
func grow(i recipe.Item, count int) recipe.Item {
switch i := i.(type) {
case item.Stack:
return i.Grow(count)
case recipe.ItemTag:
return recipe.NewItemTag(i.Tag(), i.Count()+count)
}
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")
}