331 lines
12 KiB
Go
331 lines
12 KiB
Go
|
|
package session
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"math/rand/v2"
|
||
|
|
"slices"
|
||
|
|
|
||
|
|
"github.com/df-mc/dragonfly/server/block"
|
||
|
|
"github.com/df-mc/dragonfly/server/block/cube"
|
||
|
|
"github.com/df-mc/dragonfly/server/internal/sliceutil"
|
||
|
|
"github.com/df-mc/dragonfly/server/item"
|
||
|
|
"github.com/df-mc/dragonfly/server/world"
|
||
|
|
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||
|
|
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
// enchantingInputSlot is the slot index of the input item in the enchanting table.
|
||
|
|
enchantingInputSlot = 0x0e
|
||
|
|
// enchantingLapisSlot is the slot index of the lapis in the enchanting table.
|
||
|
|
enchantingLapisSlot = 0x0f
|
||
|
|
)
|
||
|
|
|
||
|
|
// handleEnchant handles the enchantment of an item using the CraftRecipe stack request action.
|
||
|
|
func (h *ItemStackRequestHandler) handleEnchant(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
// First ensure that the selected slot is not out of bounds.
|
||
|
|
if a.RecipeNetworkID > 2 {
|
||
|
|
return fmt.Errorf("invalid recipe network id: %d", a.RecipeNetworkID)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now ensure we have an input and only one input.
|
||
|
|
input, err := s.ui.Item(enchantingInputSlot)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if input.Count() > 1 {
|
||
|
|
return fmt.Errorf("enchanting tables only accept one item at a time")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine the available enchantments using the session's enchantment seed.
|
||
|
|
allCosts, allEnchants := s.determineAvailableEnchantments(tx, c, *s.openedPos.Load(), input)
|
||
|
|
if len(allEnchants) == 0 {
|
||
|
|
return fmt.Errorf("can't enchant non-enchantable item")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use the slot plus one as the cost. The requirement and enchantments can be found in the results from
|
||
|
|
// determineAvailableEnchantments using the same slot index.
|
||
|
|
cost := int(a.RecipeNetworkID + 1)
|
||
|
|
requirement := allCosts[a.RecipeNetworkID]
|
||
|
|
enchants := allEnchants[a.RecipeNetworkID]
|
||
|
|
|
||
|
|
// If we don't have infinite resources, we need to deduct Lapis Lazuli and experience.
|
||
|
|
if !c.GameMode().CreativeInventory() {
|
||
|
|
// First ensure that the experience level is both underneath the requirement and the cost.
|
||
|
|
if c.ExperienceLevel() < requirement {
|
||
|
|
return fmt.Errorf("not enough levels to meet requirement")
|
||
|
|
}
|
||
|
|
if c.ExperienceLevel() < cost {
|
||
|
|
return fmt.Errorf("not enough levels to meet cost")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Then ensure that the player has input Lapis Lazuli, and enough of it to meet the cost.
|
||
|
|
lapis, err := s.ui.Item(enchantingLapisSlot)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if _, ok := lapis.Item().(item.LapisLazuli); !ok {
|
||
|
|
return fmt.Errorf("lapis lazuli was not input")
|
||
|
|
}
|
||
|
|
if lapis.Count() < cost {
|
||
|
|
return fmt.Errorf("not enough lapis lazuli to meet cost")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deduct the experience and Lapis Lazuli.
|
||
|
|
c.SetExperienceLevel(c.ExperienceLevel() - cost)
|
||
|
|
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||
|
|
Container: protocol.FullContainerName{ContainerID: protocol.ContainerEnchantingMaterial},
|
||
|
|
Slot: enchantingLapisSlot,
|
||
|
|
}, lapis.Grow(-cost), s, tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reset the enchantment seed so different enchantments can be selected.
|
||
|
|
c.ResetEnchantmentSeed()
|
||
|
|
|
||
|
|
// Clear the existing input item, and apply the new item into the crafting result slot of the UI. The client will
|
||
|
|
// automatically move the item into the input slot.
|
||
|
|
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||
|
|
Container: protocol.FullContainerName{ContainerID: protocol.ContainerEnchantingInput},
|
||
|
|
Slot: enchantingInputSlot,
|
||
|
|
}, item.Stack{}, s, tx)
|
||
|
|
|
||
|
|
return h.createResults(s, tx, input.WithEnchantments(enchants...))
|
||
|
|
}
|
||
|
|
|
||
|
|
// sendEnchantmentOptions sends a list of available enchantments to the client based on the client's enchantment seed
|
||
|
|
// and nearby bookshelves.
|
||
|
|
func (s *Session) sendEnchantmentOptions(tx *world.Tx, c Controllable, pos cube.Pos, stack item.Stack) {
|
||
|
|
// First determine the available enchantments for the given item stack.
|
||
|
|
selectedCosts, selectedEnchants := s.determineAvailableEnchantments(tx, c, pos, stack)
|
||
|
|
if len(selectedEnchants) == 0 {
|
||
|
|
// No available enchantments.
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build the protocol variant of the enchantment options.
|
||
|
|
options := make([]protocol.EnchantmentOption, 0, 3)
|
||
|
|
for i := 0; i < 3; i++ {
|
||
|
|
// First build the enchantment instances for each selected enchantment.
|
||
|
|
enchants := make([]protocol.EnchantmentInstance, 0, len(selectedEnchants[i]))
|
||
|
|
for _, enchant := range selectedEnchants[i] {
|
||
|
|
id, _ := item.EnchantmentID(enchant.Type())
|
||
|
|
enchants = append(enchants, protocol.EnchantmentInstance{
|
||
|
|
Type: byte(id),
|
||
|
|
Level: byte(enchant.Level()),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Then build the enchantment option. We can use the slot as the RecipeNetworkID, since the IDs seem to be unique
|
||
|
|
// to enchanting tables only. We also only need to set the middle index of Enchantments. The other two serve
|
||
|
|
// an unknown purpose and can cause various unexpected issues.
|
||
|
|
options = append(options, protocol.EnchantmentOption{
|
||
|
|
Name: enchantNames[rand.IntN(len(enchantNames))],
|
||
|
|
Cost: uint8(selectedCosts[i]),
|
||
|
|
RecipeNetworkID: uint32(i),
|
||
|
|
Enchantments: protocol.ItemEnchantments{
|
||
|
|
Slot: int32(i),
|
||
|
|
Enchantments: [3][]protocol.EnchantmentInstance{1: enchants},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Send the enchantment options to the client.
|
||
|
|
s.writePacket(&packet.PlayerEnchantOptions{Options: options})
|
||
|
|
}
|
||
|
|
|
||
|
|
// determineAvailableEnchantments returns a list of pseudo-random enchantments for the given item stack.
|
||
|
|
func (s *Session) determineAvailableEnchantments(tx *world.Tx, c Controllable, pos cube.Pos, stack item.Stack) ([]int, [][]item.Enchantment) {
|
||
|
|
// First ensure that the item is enchantable and does not already have any enchantments.
|
||
|
|
enchantable, ok := stack.Item().(item.Enchantable)
|
||
|
|
if !ok {
|
||
|
|
// We can't enchant this item.
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
if len(stack.Enchantments()) > 0 {
|
||
|
|
// We can't enchant this item.
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Search for bookshelves around the enchanting table. Bookshelves help boost the value of the enchantments that
|
||
|
|
// are selected, resulting in enchantments that are rarer but also more expensive.
|
||
|
|
seed := uint64(c.EnchantmentSeed())
|
||
|
|
random := rand.New(rand.NewPCG(seed, seed))
|
||
|
|
bookshelves := searchBookshelves(tx, pos)
|
||
|
|
value := enchantable.EnchantmentValue()
|
||
|
|
|
||
|
|
// Calculate the base cost, used to calculate the upper, middle, and lower level costs.
|
||
|
|
baseCost := random.IntN(8) + 1 + (bookshelves >> 1) + random.IntN(bookshelves+1)
|
||
|
|
|
||
|
|
// Calculate the upper, middle, and lower level costs.
|
||
|
|
upperLevelCost := max(baseCost/3, 1)
|
||
|
|
middleLevelCost := baseCost*2/3 + 1
|
||
|
|
lowerLevelCost := max(baseCost, bookshelves*2)
|
||
|
|
|
||
|
|
// Create a list of available enchantments for each slot.
|
||
|
|
return []int{
|
||
|
|
upperLevelCost,
|
||
|
|
middleLevelCost,
|
||
|
|
lowerLevelCost,
|
||
|
|
}, [][]item.Enchantment{
|
||
|
|
createEnchantments(random, stack, value, upperLevelCost),
|
||
|
|
createEnchantments(random, stack, value, middleLevelCost),
|
||
|
|
createEnchantments(random, stack, value, lowerLevelCost),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// treasureEnchantment represents an enchantment that may be a treasure enchantment.
|
||
|
|
type treasureEnchantment interface {
|
||
|
|
item.EnchantmentType
|
||
|
|
Treasure() bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// createEnchantments creates a list of enchantments for the given item stack and returns them.
|
||
|
|
func createEnchantments(random *rand.Rand, stack item.Stack, value, level int) []item.Enchantment {
|
||
|
|
// Calculate the "random bonus" for this level. This factor is used in calculating the enchantment cost, used
|
||
|
|
// during the selection of enchantments.
|
||
|
|
randomBonus := (random.Float64() + random.Float64() - 1.0) * 0.15
|
||
|
|
|
||
|
|
// Calculate the enchantment cost and clamp it to ensure it is always at least one with triangular distribution.
|
||
|
|
cost := level + 1 + random.IntN(value/4+1) + random.IntN(value/4+1)
|
||
|
|
cost = clamp(int(math.Round(float64(cost)+float64(cost)*randomBonus)), 1, math.MaxInt32)
|
||
|
|
|
||
|
|
// Books are applicable to all enchantments, so make sure we have a flag for them here.
|
||
|
|
it := stack.Item()
|
||
|
|
_, book := it.(item.Book)
|
||
|
|
|
||
|
|
// Now that we have our enchantment cost, we need to select the available enchantments. First, we iterate through
|
||
|
|
// each possible enchantment.
|
||
|
|
availableEnchants := make([]item.Enchantment, 0, len(item.Enchantments()))
|
||
|
|
for _, enchant := range item.Enchantments() {
|
||
|
|
if t, ok := enchant.(treasureEnchantment); ok && t.Treasure() {
|
||
|
|
// We then have to ensure that the enchantment is not a treasure enchantment, as those cannot be selected through
|
||
|
|
// the enchanting table.
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !book && !enchant.CompatibleWithItem(it) {
|
||
|
|
// The enchantment is not compatible with the item.
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now iterate through each possible level of the enchantment.
|
||
|
|
for i := enchant.MaxLevel(); i > 0; i-- {
|
||
|
|
// Use the level to calculate the minimum and maximum costs for this enchantment.
|
||
|
|
if minCost, maxCost := enchant.Cost(i); cost >= minCost && cost <= maxCost {
|
||
|
|
// If the cost is within the bounds, add the enchantment to the list of available enchantments.
|
||
|
|
availableEnchants = append(availableEnchants, item.NewEnchantment(enchant, i))
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(availableEnchants) == 0 {
|
||
|
|
// No available enchantments, so we can't really do much here.
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now we need to select the enchantments.
|
||
|
|
selectedEnchants := make([]item.Enchantment, 0, len(availableEnchants))
|
||
|
|
|
||
|
|
// Select the first enchantment using a weighted random algorithm, favouring enchantments that have a higher weight.
|
||
|
|
// These weights are based on the enchantment's rarity, with common and uncommon enchantments having a higher weight
|
||
|
|
// than rare and very rare enchantments.
|
||
|
|
enchant := weightedRandomEnchantment(random, availableEnchants)
|
||
|
|
selectedEnchants = append(selectedEnchants, enchant)
|
||
|
|
|
||
|
|
// Remove the selected enchantment from the list of available enchantments, so we don't select it again.
|
||
|
|
ind := slices.Index(availableEnchants, enchant)
|
||
|
|
availableEnchants = slices.Delete(availableEnchants, ind, ind+1)
|
||
|
|
|
||
|
|
// Based on the cost, select a random amount of additional enchantments.
|
||
|
|
for random.IntN(50) <= cost {
|
||
|
|
// Ensure that we don't have any conflicting enchantments. If so, remove them from the list of available
|
||
|
|
// enchantments.
|
||
|
|
lastEnchant := selectedEnchants[len(selectedEnchants)-1]
|
||
|
|
if availableEnchants = sliceutil.Filter(availableEnchants, func(enchant item.Enchantment) bool {
|
||
|
|
return lastEnchant.Type().CompatibleWithEnchantment(enchant.Type())
|
||
|
|
}); len(availableEnchants) == 0 {
|
||
|
|
// We've exhausted all available enchantments.
|
||
|
|
break
|
||
|
|
}
|
||
|
|
|
||
|
|
// Select another enchantment using the same weighted random algorithm.
|
||
|
|
enchant = weightedRandomEnchantment(random, availableEnchants)
|
||
|
|
selectedEnchants = append(selectedEnchants, enchant)
|
||
|
|
|
||
|
|
// Remove the selected enchantment from the list of available enchantments, so we don't select it again.
|
||
|
|
ind = slices.Index(availableEnchants, enchant)
|
||
|
|
availableEnchants = slices.Delete(availableEnchants, ind, ind+1)
|
||
|
|
|
||
|
|
// Halve the cost, so we have a lower chance of selecting another enchantment.
|
||
|
|
cost /= 2
|
||
|
|
}
|
||
|
|
return selectedEnchants
|
||
|
|
}
|
||
|
|
|
||
|
|
// searchBookshelves searches for nearby bookshelves around the position passed, and returns the amount found.
|
||
|
|
func searchBookshelves(tx *world.Tx, pos cube.Pos) (shelves int) {
|
||
|
|
for x := -1; x <= 1; x++ {
|
||
|
|
for z := -1; z <= 1; z++ {
|
||
|
|
for y := 0; y <= 1; y++ {
|
||
|
|
if x == 0 && z == 0 {
|
||
|
|
// Ignore the centre block.
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, ok := tx.Block(pos.Add(cube.Pos{x, y, z})).(block.Air); !ok {
|
||
|
|
// There must be a one block space between the bookshelf and the player.
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for a bookshelf two blocks away.
|
||
|
|
if _, ok := tx.Block(pos.Add(cube.Pos{x * 2, y, z * 2})).(block.Bookshelf); ok {
|
||
|
|
shelves++
|
||
|
|
}
|
||
|
|
if x != 0 && z != 0 {
|
||
|
|
// Check for a bookshelf two blocks away on the X axis.
|
||
|
|
if _, ok := tx.Block(pos.Add(cube.Pos{x * 2, y, z})).(block.Bookshelf); ok {
|
||
|
|
shelves++
|
||
|
|
}
|
||
|
|
// Check for a bookshelf two blocks away on the Z axis.
|
||
|
|
if _, ok := tx.Block(pos.Add(cube.Pos{x, y, z * 2})).(block.Bookshelf); ok {
|
||
|
|
shelves++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if shelves >= 15 {
|
||
|
|
// We've found enough bookshelves.
|
||
|
|
return 15
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return shelves
|
||
|
|
}
|
||
|
|
|
||
|
|
// weightedRandomEnchantment returns a random enchantment from the given list of enchantments using the rarity weight of
|
||
|
|
// each enchantment.
|
||
|
|
func weightedRandomEnchantment(rs *rand.Rand, enchants []item.Enchantment) item.Enchantment {
|
||
|
|
var totalWeight int
|
||
|
|
for _, e := range enchants {
|
||
|
|
totalWeight += e.Type().Rarity().Weight()
|
||
|
|
}
|
||
|
|
r := rs.IntN(totalWeight)
|
||
|
|
for _, e := range enchants {
|
||
|
|
r -= e.Type().Rarity().Weight()
|
||
|
|
if r < 0 {
|
||
|
|
return e
|
||
|
|
}
|
||
|
|
}
|
||
|
|
panic("should never happen")
|
||
|
|
}
|
||
|
|
|
||
|
|
// clamp clamps a value into the given range.
|
||
|
|
func clamp(value, min, max int) int {
|
||
|
|
if value < min {
|
||
|
|
return min
|
||
|
|
}
|
||
|
|
if value > max {
|
||
|
|
return max
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
}
|