313 lines
9.5 KiB
Go
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")
|
||
|
|
}
|