github.com/df-mc/dragonfly@v0.9.13/server/session/handler_enchanting.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/block/cube" 7 "github.com/df-mc/dragonfly/server/internal/sliceutil" 8 "github.com/df-mc/dragonfly/server/item" 9 "github.com/df-mc/dragonfly/server/world" 10 "github.com/sandertv/gophertunnel/minecraft/protocol" 11 "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 12 "math" 13 "math/rand" 14 "slices" 15 ) 16 17 const ( 18 // enchantingInputSlot is the slot index of the input item in the enchanting table. 19 enchantingInputSlot = 0x0e 20 // enchantingLapisSlot is the slot index of the lapis in the enchanting table. 21 enchantingLapisSlot = 0x0f 22 ) 23 24 // handleEnchant handles the enchantment of an item using the CraftRecipe stack request action. 25 func (h *ItemStackRequestHandler) handleEnchant(a *protocol.CraftRecipeStackRequestAction, s *Session) error { 26 // First ensure that the selected slot is not out of bounds. 27 if a.RecipeNetworkID > 2 { 28 return fmt.Errorf("invalid recipe network id: %d", a.RecipeNetworkID) 29 } 30 31 // Now ensure we have an input and only one input. 32 input, err := s.ui.Item(enchantingInputSlot) 33 if err != nil { 34 return err 35 } 36 if input.Count() > 1 { 37 return fmt.Errorf("enchanting tables only accept one item at a time") 38 } 39 40 // Determine the available enchantments using the session's enchantment seed. 41 allCosts, allEnchants := s.determineAvailableEnchantments(s.c.World(), s.openedPos.Load(), input) 42 if len(allEnchants) == 0 { 43 return fmt.Errorf("can't enchant non-enchantable item") 44 } 45 46 // Use the slot plus one as the cost. The requirement and enchantments can be found in the results from 47 // determineAvailableEnchantments using the same slot index. 48 cost := int(a.RecipeNetworkID + 1) 49 requirement := allCosts[a.RecipeNetworkID] 50 enchants := allEnchants[a.RecipeNetworkID] 51 52 // If we don't have infinite resources, we need to deduct Lapis Lazuli and experience. 53 if !s.c.GameMode().CreativeInventory() { 54 // First ensure that the experience level is both underneath the requirement and the cost. 55 if s.c.ExperienceLevel() < requirement { 56 return fmt.Errorf("not enough levels to meet requirement") 57 } 58 if s.c.ExperienceLevel() < cost { 59 return fmt.Errorf("not enough levels to meet cost") 60 } 61 62 // Then ensure that the player has input Lapis Lazuli, and enough of it to meet the cost. 63 lapis, err := s.ui.Item(enchantingLapisSlot) 64 if err != nil { 65 return err 66 } 67 if _, ok := lapis.Item().(item.LapisLazuli); !ok { 68 return fmt.Errorf("lapis lazuli was not input") 69 } 70 if lapis.Count() < cost { 71 return fmt.Errorf("not enough lapis lazuli to meet cost") 72 } 73 74 // Deduct the experience and Lapis Lazuli. 75 s.c.SetExperienceLevel(s.c.ExperienceLevel() - cost) 76 h.setItemInSlot(protocol.StackRequestSlotInfo{ 77 ContainerID: protocol.ContainerEnchantingMaterial, 78 Slot: enchantingLapisSlot, 79 }, lapis.Grow(-cost), s) 80 } 81 82 // Reset the enchantment seed so different enchantments can be selected. 83 s.c.ResetEnchantmentSeed() 84 85 // Clear the existing input item, and apply the new item into the crafting result slot of the UI. The client will 86 // automatically move the item into the input slot. 87 h.setItemInSlot(protocol.StackRequestSlotInfo{ 88 ContainerID: protocol.ContainerEnchantingInput, 89 Slot: enchantingInputSlot, 90 }, item.Stack{}, s) 91 92 return h.createResults(s, input.WithEnchantments(enchants...)) 93 } 94 95 // sendEnchantmentOptions sends a list of available enchantments to the client based on the client's enchantment seed 96 // and nearby bookshelves. 97 func (s *Session) sendEnchantmentOptions(w *world.World, pos cube.Pos, stack item.Stack) { 98 // First determine the available enchantments for the given item stack. 99 selectedCosts, selectedEnchants := s.determineAvailableEnchantments(w, pos, stack) 100 if len(selectedEnchants) == 0 { 101 // No available enchantments. 102 return 103 } 104 105 // Build the protocol variant of the enchantment options. 106 options := make([]protocol.EnchantmentOption, 0, 3) 107 for i := 0; i < 3; i++ { 108 // First build the enchantment instances for each selected enchantment. 109 enchants := make([]protocol.EnchantmentInstance, 0, len(selectedEnchants[i])) 110 for _, enchant := range selectedEnchants[i] { 111 id, _ := item.EnchantmentID(enchant.Type()) 112 enchants = append(enchants, protocol.EnchantmentInstance{ 113 Type: byte(id), 114 Level: byte(enchant.Level()), 115 }) 116 } 117 118 // Then build the enchantment option. We can use the slot as the RecipeNetworkID, since the IDs seem to be unique 119 // to enchanting tables only. We also only need to set the middle index of Enchantments. The other two serve 120 // an unknown purpose and can cause various unexpected issues. 121 options = append(options, protocol.EnchantmentOption{ 122 Name: enchantNames[rand.Intn(len(enchantNames))], 123 Cost: uint32(selectedCosts[i]), 124 RecipeNetworkID: uint32(i), 125 Enchantments: protocol.ItemEnchantments{ 126 Slot: int32(i), 127 Enchantments: [3][]protocol.EnchantmentInstance{1: enchants}, 128 }, 129 }) 130 } 131 132 // Send the enchantment options to the client. 133 s.writePacket(&packet.PlayerEnchantOptions{Options: options}) 134 } 135 136 // determineAvailableEnchantments returns a list of pseudo-random enchantments for the given item stack. 137 func (s *Session) determineAvailableEnchantments(w *world.World, pos cube.Pos, stack item.Stack) ([]int, [][]item.Enchantment) { 138 // First ensure that the item is enchantable and does not already have any enchantments. 139 enchantable, ok := stack.Item().(item.Enchantable) 140 if !ok { 141 // We can't enchant this item. 142 return nil, nil 143 } 144 if len(stack.Enchantments()) > 0 { 145 // We can't enchant this item. 146 return nil, nil 147 } 148 149 // Search for bookshelves around the enchanting table. Bookshelves help boost the value of the enchantments that 150 // are selected, resulting in enchantments that are rarer but also more expensive. 151 random := rand.New(rand.NewSource(s.c.EnchantmentSeed())) 152 bookshelves := searchBookshelves(w, pos) 153 value := enchantable.EnchantmentValue() 154 155 // Calculate the base cost, used to calculate the upper, middle, and lower level costs. 156 baseCost := random.Intn(8) + 1 + (bookshelves >> 1) + random.Intn(bookshelves+1) 157 158 // Calculate the upper, middle, and lower level costs. 159 upperLevelCost := max(baseCost/3, 1) 160 middleLevelCost := baseCost*2/3 + 1 161 lowerLevelCost := max(baseCost, bookshelves*2) 162 163 // Create a list of available enchantments for each slot. 164 return []int{ 165 upperLevelCost, 166 middleLevelCost, 167 lowerLevelCost, 168 }, [][]item.Enchantment{ 169 createEnchantments(random, stack, value, upperLevelCost), 170 createEnchantments(random, stack, value, middleLevelCost), 171 createEnchantments(random, stack, value, lowerLevelCost), 172 } 173 } 174 175 // treasureEnchantment represents an enchantment that may be a treasure enchantment. 176 type treasureEnchantment interface { 177 item.EnchantmentType 178 Treasure() bool 179 } 180 181 // createEnchantments creates a list of enchantments for the given item stack and returns them. 182 func createEnchantments(random *rand.Rand, stack item.Stack, value, level int) []item.Enchantment { 183 // Calculate the "random bonus" for this level. This factor is used in calculating the enchantment cost, used 184 // during the selection of enchantments. 185 randomBonus := (random.Float64() + random.Float64() - 1.0) * 0.15 186 187 // Calculate the enchantment cost and clamp it to ensure it is always at least one with triangular distribution. 188 cost := level + 1 + random.Intn(value/4+1) + random.Intn(value/4+1) 189 cost = clamp(int(math.Round(float64(cost)+float64(cost)*randomBonus)), 1, math.MaxInt32) 190 191 // Books are applicable to all enchantments, so make sure we have a flag for them here. 192 it := stack.Item() 193 _, book := it.(item.Book) 194 195 // Now that we have our enchantment cost, we need to select the available enchantments. First, we iterate through 196 // each possible enchantment. 197 availableEnchants := make([]item.Enchantment, 0, len(item.Enchantments())) 198 for _, enchant := range item.Enchantments() { 199 if t, ok := enchant.(treasureEnchantment); ok && t.Treasure() { 200 // We then have to ensure that the enchantment is not a treasure enchantment, as those cannot be selected through 201 // the enchanting table. 202 continue 203 } 204 if !book && !enchant.CompatibleWithItem(it) { 205 // The enchantment is not compatible with the item. 206 continue 207 } 208 209 // Now iterate through each possible level of the enchantment. 210 for i := enchant.MaxLevel(); i > 0; i-- { 211 // Use the level to calculate the minimum and maximum costs for this enchantment. 212 if minCost, maxCost := enchant.Cost(i); cost >= minCost && cost <= maxCost { 213 // If the cost is within the bounds, add the enchantment to the list of available enchantments. 214 availableEnchants = append(availableEnchants, item.NewEnchantment(enchant, i)) 215 break 216 } 217 } 218 } 219 if len(availableEnchants) == 0 { 220 // No available enchantments, so we can't really do much here. 221 return nil 222 } 223 224 // Now we need to select the enchantments. 225 selectedEnchants := make([]item.Enchantment, 0, len(availableEnchants)) 226 227 // Select the first enchantment using a weighted random algorithm, favouring enchantments that have a higher weight. 228 // These weights are based on the enchantment's rarity, with common and uncommon enchantments having a higher weight 229 // than rare and very rare enchantments. 230 enchant := weightedRandomEnchantment(random, availableEnchants) 231 selectedEnchants = append(selectedEnchants, enchant) 232 233 // Remove the selected enchantment from the list of available enchantments, so we don't select it again. 234 ind := sliceutil.Index(availableEnchants, enchant) 235 availableEnchants = slices.Delete(availableEnchants, ind, ind+1) 236 237 // Based on the cost, select a random amount of additional enchantments. 238 for random.Intn(50) <= cost { 239 // Ensure that we don't have any conflicting enchantments. If so, remove them from the list of available 240 // enchantments. 241 lastEnchant := selectedEnchants[len(selectedEnchants)-1] 242 if availableEnchants = sliceutil.Filter(availableEnchants, func(enchant item.Enchantment) bool { 243 return lastEnchant.Type().CompatibleWithEnchantment(enchant.Type()) 244 }); len(availableEnchants) == 0 { 245 // We've exhausted all available enchantments. 246 break 247 } 248 249 // Select another enchantment using the same weighted random algorithm. 250 enchant = weightedRandomEnchantment(random, availableEnchants) 251 selectedEnchants = append(selectedEnchants, enchant) 252 253 // Remove the selected enchantment from the list of available enchantments, so we don't select it again. 254 ind = sliceutil.Index(availableEnchants, enchant) 255 availableEnchants = slices.Delete(availableEnchants, ind, ind+1) 256 257 // Halve the cost, so we have a lower chance of selecting another enchantment. 258 cost /= 2 259 } 260 return selectedEnchants 261 } 262 263 // searchBookshelves searches for nearby bookshelves around the position passed, and returns the amount found. 264 func searchBookshelves(w *world.World, pos cube.Pos) (shelves int) { 265 for x := -1; x <= 1; x++ { 266 for z := -1; z <= 1; z++ { 267 for y := 0; y <= 1; y++ { 268 if x == 0 && z == 0 { 269 // Ignore the center block. 270 continue 271 } 272 if _, ok := w.Block(pos.Add(cube.Pos{x, y, z})).(block.Air); !ok { 273 // There must be a one block space between the bookshelf and the player. 274 continue 275 } 276 277 // Check for a bookshelf two blocks away. 278 if _, ok := w.Block(pos.Add(cube.Pos{x * 2, y, z * 2})).(block.Bookshelf); ok { 279 shelves++ 280 } 281 if x != 0 && z != 0 { 282 // Check for a bookshelf two blocks away on the X axis. 283 if _, ok := w.Block(pos.Add(cube.Pos{x * 2, y, z})).(block.Bookshelf); ok { 284 shelves++ 285 } 286 // Check for a bookshelf two blocks away on the Z axis. 287 if _, ok := w.Block(pos.Add(cube.Pos{x, y, z * 2})).(block.Bookshelf); ok { 288 shelves++ 289 } 290 } 291 292 if shelves >= 15 { 293 // We've found enough bookshelves. 294 return 15 295 } 296 } 297 } 298 } 299 return shelves 300 } 301 302 // weightedRandomEnchantment returns a random enchantment from the given list of enchantments using the rarity weight of 303 // each enchantment. 304 func weightedRandomEnchantment(rs *rand.Rand, enchants []item.Enchantment) item.Enchantment { 305 var totalWeight int 306 for _, e := range enchants { 307 totalWeight += e.Type().Rarity().Weight() 308 } 309 r := rs.Intn(totalWeight) 310 for _, e := range enchants { 311 r -= e.Type().Rarity().Weight() 312 if r < 0 { 313 return e 314 } 315 } 316 panic("should never happen") 317 } 318 319 // clamp clamps a value into the given range. 320 func clamp(value, min, max int) int { 321 if value < min { 322 return min 323 } 324 if value > max { 325 return max 326 } 327 return value 328 }