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  }