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 }