up3
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
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
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user