github.com/df-mc/dragonfly@v0.9.13/server/session/handler_anvil.go (about)

     1  package session
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/df-mc/dragonfly/server/block"
     6  	"github.com/df-mc/dragonfly/server/item"
     7  	"github.com/df-mc/dragonfly/server/world/sound"
     8  	"github.com/sandertv/gophertunnel/minecraft/protocol"
     9  	"math/rand"
    10  )
    11  
    12  const (
    13  	// anvilInputSlot is the slot index of the input item in the anvil.
    14  	anvilInputSlot = 0x1
    15  	// anvilMaterialSlot is the slot index of the material in the anvil.
    16  	anvilMaterialSlot = 0x2
    17  )
    18  
    19  // handleCraftRecipeOptional handles the CraftRecipeOptional request action, sent when taking a result from an anvil
    20  // menu. It also contains information such as the new name of the item and the multi-recipe network ID.
    21  func (h *ItemStackRequestHandler) handleCraftRecipeOptional(a *protocol.CraftRecipeOptionalStackRequestAction, s *Session, filterStrings []string) (err error) {
    22  	// First check if there actually is an anvil opened.
    23  	if !s.containerOpened.Load() {
    24  		return fmt.Errorf("no anvil container opened")
    25  	}
    26  
    27  	w := s.c.World()
    28  	pos := s.openedPos.Load()
    29  	anvil, ok := w.Block(pos).(block.Anvil)
    30  	if !ok {
    31  		return fmt.Errorf("no anvil container opened")
    32  	}
    33  	if len(filterStrings) < int(a.FilterStringIndex) {
    34  		return fmt.Errorf("filter string index %v is out of bounds", a.FilterStringIndex)
    35  	}
    36  
    37  	input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
    38  		ContainerID: protocol.ContainerAnvilInput,
    39  		Slot:        anvilInputSlot,
    40  	}, s)
    41  	if input.Empty() {
    42  		return fmt.Errorf("no item in input input slot")
    43  	}
    44  	material, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
    45  		ContainerID: protocol.ContainerAnvilMaterial,
    46  		Slot:        anvilMaterialSlot,
    47  	}, s)
    48  	result := input
    49  
    50  	// The sum of the input's anvil cost as well as the material's anvil cost.
    51  	anvilCost := input.AnvilCost()
    52  	if !material.Empty() {
    53  		anvilCost += material.AnvilCost()
    54  	}
    55  
    56  	// The material input may be empty (if the player is only renaming, for example).
    57  	var actionCost, renameCost, repairCount int
    58  	if !material.Empty() {
    59  		// First check if we are trying to repair the item with a material.
    60  		if repairable, ok := input.Item().(item.Repairable); ok && repairable.RepairableBy(material) {
    61  			result, actionCost, repairCount, err = repairItemWithMaterial(input, material, result)
    62  			if err != nil {
    63  				return err
    64  			}
    65  		} else {
    66  			_, book := material.Item().(item.EnchantedBook)
    67  			_, durable := input.Item().(item.Durable)
    68  
    69  			// Ensure that the input item is repairable, or the material item is an enchanted book. If not, this is an
    70  			// invalid scenario, and we should return an error.
    71  			enchantedBook := book && len(material.Enchantments()) > 0
    72  			if !enchantedBook && (input.Item() != material.Item() || !durable) {
    73  				return fmt.Errorf("input item is not repairable/same type or material item is not an enchanted book")
    74  			}
    75  
    76  			// If the material is another durable item, we just need to increase the durability of the result by the
    77  			// material's durability at 12%.
    78  			if durable && !enchantedBook {
    79  				result, actionCost = repairItemWithDurable(input, material, result)
    80  			}
    81  
    82  			// Merge enchantments on the material item onto the result item.
    83  			var hasCompatible, hasIncompatible bool
    84  			result, hasCompatible, hasIncompatible, actionCost = mergeEnchantments(input, material, result, actionCost, enchantedBook)
    85  
    86  			// If we don't have any compatible enchantments and the input item isn't durable, then this is an invalid
    87  			// scenario, and we should return an error.
    88  			if !durable && hasIncompatible && !hasCompatible {
    89  				return fmt.Errorf("no compatible enchantments but have incompatible ones")
    90  			}
    91  		}
    92  	}
    93  
    94  	// If we have a filter string, then the client is intending to rename the item.
    95  	if len(filterStrings) > 0 {
    96  		renameCost = 1
    97  		actionCost += renameCost
    98  		result = result.WithCustomName(filterStrings[int(a.FilterStringIndex)])
    99  	}
   100  
   101  	// Calculate the total cost. (action cost + anvil cost)
   102  	cost := actionCost + anvilCost
   103  	if cost <= 0 {
   104  		return fmt.Errorf("no action was taken")
   105  	}
   106  
   107  	// If our only action was renaming, the cost should never exceed 40.
   108  	if renameCost == actionCost && renameCost > 0 && cost >= 40 {
   109  		cost = 39
   110  	}
   111  
   112  	// We can bypass the "impossible cost" limit if we're in creative mode.
   113  	c := s.c.GameMode().CreativeInventory()
   114  	if cost >= 40 && !c {
   115  		return fmt.Errorf("impossible cost")
   116  	}
   117  
   118  	// Ensure we have enough levels (or if we're in creative mode, ignore the cost) to perform the action.
   119  	level := s.c.ExperienceLevel()
   120  	if level < cost && !c {
   121  		return fmt.Errorf("not enough experience")
   122  	} else if !c {
   123  		s.c.SetExperienceLevel(level - cost)
   124  	}
   125  
   126  	// If we had a result item, we need to calculate the new anvil cost and update it on the item.
   127  	if !result.Empty() {
   128  		updatedAnvilCost := result.AnvilCost()
   129  		if !material.Empty() && updatedAnvilCost < material.AnvilCost() {
   130  			updatedAnvilCost = material.AnvilCost()
   131  		}
   132  		if renameCost != actionCost || renameCost == 0 {
   133  			updatedAnvilCost = updatedAnvilCost*2 + 1
   134  		}
   135  		result = result.WithAnvilCost(updatedAnvilCost)
   136  	}
   137  
   138  	// If we're not in creative mode, we have a 12% chance of the anvil degrading down one state. If that is the case, we
   139  	// need to play the related sound and update the block state. Otherwise, we play a regular anvil use sound.
   140  	if !c && rand.Float64() < 0.12 {
   141  		damaged := anvil.Break()
   142  		if _, ok := damaged.(block.Air); ok {
   143  			w.PlaySound(pos.Vec3Centre(), sound.AnvilBreak{})
   144  		} else {
   145  			w.PlaySound(pos.Vec3Centre(), sound.AnvilUse{})
   146  		}
   147  		defer w.SetBlock(pos, damaged, nil)
   148  	} else {
   149  		w.PlaySound(pos.Vec3Centre(), sound.AnvilUse{})
   150  	}
   151  
   152  	h.setItemInSlot(protocol.StackRequestSlotInfo{
   153  		ContainerID: protocol.ContainerAnvilInput,
   154  		Slot:        anvilInputSlot,
   155  	}, item.Stack{}, s)
   156  	if repairCount > 0 {
   157  		h.setItemInSlot(protocol.StackRequestSlotInfo{
   158  			ContainerID: protocol.ContainerAnvilMaterial,
   159  			Slot:        anvilMaterialSlot,
   160  		}, material.Grow(-repairCount), s)
   161  	} else {
   162  		h.setItemInSlot(protocol.StackRequestSlotInfo{
   163  			ContainerID: protocol.ContainerAnvilMaterial,
   164  			Slot:        anvilMaterialSlot,
   165  		}, item.Stack{}, s)
   166  	}
   167  	return h.createResults(s, result)
   168  }
   169  
   170  // repairItemWithMaterial is a helper function that repairs an item stack with a given material stack. It returns the new item
   171  // stack, the cost, and the repaired items count.
   172  func repairItemWithMaterial(input item.Stack, material item.Stack, result item.Stack) (item.Stack, int, int, error) {
   173  	// Calculate the durability delta using the maximum durability and the current durability.
   174  	delta := min(input.MaxDurability()-input.Durability(), input.MaxDurability()/4)
   175  	if delta <= 0 {
   176  		return item.Stack{}, 0, 0, fmt.Errorf("input item is already fully repaired")
   177  	}
   178  
   179  	// While the durability delta is more than zero and the repaired count is under the material count, increase
   180  	// the durability of the result by the durability delta.
   181  	var cost, count int
   182  	for ; delta > 0 && count < material.Count(); count, delta = count+1, min(result.MaxDurability()-result.Durability(), result.MaxDurability()/4) {
   183  		result = result.WithDurability(result.Durability() + delta)
   184  		cost++
   185  	}
   186  	return result, cost, count, nil
   187  }
   188  
   189  // repairItemWithDurable is a helper function that repairs an item with another durable item stack.
   190  func repairItemWithDurable(input item.Stack, durable item.Stack, result item.Stack) (item.Stack, int) {
   191  	durability := input.Durability() + durable.Durability() + input.MaxDurability()*12/100
   192  	if durability > input.MaxDurability() {
   193  		durability = input.MaxDurability()
   194  	}
   195  
   196  	// Ensure the durability is higher than the input's current durability.
   197  	var cost int
   198  	if durability > input.Durability() {
   199  		result = result.WithDurability(durability)
   200  		cost += 2
   201  	}
   202  	return result, cost
   203  }
   204  
   205  // mergeEnchantments merges the enchantments of the material item stack onto the result item stack and returns the result
   206  // item stack, booleans indicating whether the enchantments had any compatible or incompatible enchantments, and the cost.
   207  func mergeEnchantments(input item.Stack, material item.Stack, result item.Stack, cost int, enchantedBook bool) (item.Stack, bool, bool, int) {
   208  	var hasCompatible, hasIncompatible bool
   209  	for _, enchant := range material.Enchantments() {
   210  		// First ensure that the enchantment type is compatible with the input item.
   211  		enchantType := enchant.Type()
   212  		compatible := enchantType.CompatibleWithItem(input.Item())
   213  		if _, ok := input.Item().(item.EnchantedBook); ok {
   214  			compatible = true
   215  		}
   216  
   217  		// Then ensure that each input enchantment is compatible with this material enchantment. If one is not compatible,
   218  		// increase the cost by one.
   219  		for _, otherEnchant := range input.Enchantments() {
   220  			if otherType := otherEnchant.Type(); enchantType != otherType && !enchantType.CompatibleWithEnchantment(otherType) {
   221  				compatible = false
   222  				cost++
   223  			}
   224  		}
   225  
   226  		// Skip the enchantment if it isn't compatible with enchantments on the input item.
   227  		if !compatible {
   228  			hasIncompatible = true
   229  			continue
   230  		}
   231  		hasCompatible = true
   232  
   233  		resultLevel := enchant.Level()
   234  		levelCost := resultLevel
   235  
   236  		// Check if we have an enchantment of the same type on the input item.
   237  		if existingEnchant, ok := input.Enchantment(enchantType); ok {
   238  			if existingEnchant.Level() > resultLevel || (existingEnchant.Level() == resultLevel && resultLevel == enchantType.MaxLevel()) {
   239  				// The result level is either lower than the existing enchantment's level or is higher than the maximum
   240  				// level, so skip this enchantment.
   241  				hasIncompatible = true
   242  				continue
   243  			} else if existingEnchant.Level() == resultLevel {
   244  				// If the input level is equal to the material level, increase the result level by one.
   245  				resultLevel++
   246  			}
   247  			// Update the level cost. (result level - existing level)
   248  			levelCost = resultLevel - existingEnchant.Level()
   249  		}
   250  
   251  		// Now calculate the rarity cost. This is just the application cost of the rarity, however if the
   252  		// material is an enchanted book, then the rarity cost gets halved. If the new rarity cost is under one,
   253  		// it is set to one.
   254  		rarityCost := enchantType.Rarity().Cost()
   255  		if enchantedBook {
   256  			rarityCost = max(1, rarityCost/2)
   257  		}
   258  
   259  		// Update the result item with the new enchantment.
   260  		result = result.WithEnchantments(item.NewEnchantment(enchantType, resultLevel))
   261  
   262  		// Update the cost appropriately.
   263  		cost += rarityCost * levelCost
   264  		if input.Count() > 1 {
   265  			cost = 40
   266  		}
   267  	}
   268  	return result, hasCompatible, hasIncompatible, cost
   269  }
   270  
   271  // max returns the max of two integers.
   272  func max(x, y int) int {
   273  	if x > y {
   274  		return x
   275  	}
   276  	return y
   277  }
   278  
   279  // min returns the min of two integers.
   280  func min(x, y int) int {
   281  	if x > y {
   282  		return y
   283  	}
   284  	return x
   285  }