Files
mc/server/session/player.go
T

1272 lines
42 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
package session
import (
"encoding/json"
"fmt"
"image/color"
"maps"
"math"
"net"
"slices"
"time"
_ "unsafe" // Imported for compiler directives.
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/entity"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/internal/nbtconv"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/creative"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/item/recipe"
"github.com/df-mc/dragonfly/server/player/debug"
"github.com/df-mc/dragonfly/server/player/dialogue"
"github.com/df-mc/dragonfly/server/player/form"
"github.com/df-mc/dragonfly/server/player/hud"
"github.com/df-mc/dragonfly/server/player/skin"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl32"
"github.com/go-gl/mathgl/mgl64"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
// StopShowingEntity stops showing a world.Entity to the Session. It will be completely invisible until a call to
// StartShowingEntity is made.
func (s *Session) StopShowingEntity(e world.Entity) {
s.entityMutex.Lock()
_, ok := s.hiddenEntities[e.H().UUID()]
if !ok {
s.hiddenEntities[e.H().UUID()] = struct{}{}
}
s.entityMutex.Unlock()
if !ok {
s.HideEntity(e)
}
}
// StartShowingEntity starts showing a world.Entity to the Session that was previously hidden using StopShowingEntity.
func (s *Session) StartShowingEntity(e world.Entity) {
s.entityMutex.Lock()
_, ok := s.hiddenEntities[e.H().UUID()]
if ok {
delete(s.hiddenEntities, e.H().UUID())
}
s.entityMutex.Unlock()
if ok {
s.ViewEntity(e)
s.ViewEntityState(e)
s.ViewEntityItems(e)
s.ViewEntityArmour(e)
}
}
// closeCurrentContainer closes the container the player might currently have open.
func (s *Session) closeCurrentContainer(tx *world.Tx, clientRequested bool) {
if !s.closeWindow(clientRequested) {
return
}
pos := *s.openedPos.Load()
b := tx.Block(pos)
if container, ok := b.(block.Container); ok {
container.RemoveViewer(s, tx, pos)
} else if enderChest, ok := b.(block.EnderChest); ok {
enderChest.RemoveViewer(tx, pos)
}
}
// SendRespawn spawns the Controllable entity of the session client-side in the world, provided it has died.
func (s *Session) SendRespawn(pos mgl64.Vec3, c Controllable) {
s.writePacket(&packet.Respawn{
Position: vec64To32(pos.Add(entityOffset(c))),
State: packet.RespawnStateReadyToSpawn,
EntityRuntimeID: selfEntityRuntimeID,
})
}
// SendPlayerSpawn updates the player's spawn point on the client-side. There is currently little reason
// to do so other than to prevent the client-side "Respawn point set" message when sleeping in a bed.
func (s *Session) SendPlayerSpawn(pos mgl64.Vec3) {
blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
s.writePacket(&packet.SetSpawnPosition{
SpawnType: packet.SpawnTypePlayer,
Position: blockPos,
Dimension: packet.DimensionOverworld,
SpawnPosition: blockPos,
})
}
// sendBiomes sends all the vanilla biomes to the session.
func (s *Session) sendBiomes() {
definitions, stringList := world.BiomeDefinitions()
s.writePacket(&packet.BiomeDefinitionList{
BiomeDefinitions: definitions,
StringList: stringList,
})
}
// sendRecipes sends the current crafting recipes to the session.
func (s *Session) sendRecipes() {
recipes := make([]protocol.Recipe, 0, len(recipe.Recipes()))
potionRecipes := make([]protocol.PotionRecipe, 0)
potionContainerChange := make([]protocol.PotionContainerChangeRecipe, 0)
for index, i := range recipe.Recipes() {
networkID := uint32(index) + 1
s.recipes[networkID] = i
switch i := i.(type) {
case recipe.Shapeless:
recipes = append(recipes, &protocol.ShapelessRecipe{
RecipeID: uuid.New().String(),
Priority: int32(i.Priority()),
Input: stacksToIngredientItems(s.br, i.Input()),
Output: stacksToRecipeStacks(s.br, i.Output()),
Block: i.Block(),
RecipeNetworkID: networkID,
})
case recipe.Shaped:
recipes = append(recipes, &protocol.ShapedRecipe{
RecipeID: uuid.New().String(),
Priority: int32(i.Priority()),
Width: int32(i.Shape().Width()),
Height: int32(i.Shape().Height()),
Input: stacksToIngredientItems(s.br, i.Input()),
Output: stacksToRecipeStacks(s.br, i.Output()),
Block: i.Block(),
RecipeNetworkID: networkID,
})
case recipe.SmithingTransform:
input, output := stacksToIngredientItems(s.br, i.Input()), stacksToRecipeStacks(s.br, i.Output())
recipes = append(recipes, &protocol.SmithingTransformRecipe{
RecipeID: uuid.New().String(),
Base: input[0],
Addition: input[1],
Template: input[2],
Result: output[0],
Block: i.Block(),
RecipeNetworkID: networkID,
})
case recipe.SmithingTrim:
input := stacksToIngredientItems(s.br, i.Input())
recipes = append(recipes, &protocol.SmithingTrimRecipe{
RecipeID: uuid.New().String(),
Base: input[0],
Addition: input[1],
Template: input[2],
Block: i.Block(),
RecipeNetworkID: networkID,
})
case recipe.Potion:
inputRuntimeID, inputMeta, _ := world.ItemRuntimeID(i.Input()[0].(item.Stack).Item())
reagentRuntimeID, reagentMeta, _ := world.ItemRuntimeID(i.Input()[1].(item.Stack).Item())
outputRuntimeID, outputMeta, _ := world.ItemRuntimeID(i.Output()[0].Item())
potionRecipes = append(potionRecipes, protocol.PotionRecipe{
InputPotionID: inputRuntimeID,
InputPotionMetadata: int32(inputMeta),
ReagentItemID: reagentRuntimeID,
ReagentItemMetadata: int32(reagentMeta),
OutputPotionID: outputRuntimeID,
OutputPotionMetadata: int32(outputMeta),
})
case recipe.PotionContainerChange:
inputRuntimeID, _, _ := world.ItemRuntimeID(i.Input()[0].(item.Stack).Item())
reagentRuntimeID, _, _ := world.ItemRuntimeID(i.Input()[1].(item.Stack).Item())
outputRuntimeID, _, _ := world.ItemRuntimeID(i.Output()[0].Item())
potionContainerChange = append(potionContainerChange, protocol.PotionContainerChangeRecipe{
InputItemID: inputRuntimeID,
ReagentItemID: reagentRuntimeID,
OutputItemID: outputRuntimeID,
})
}
}
s.writePacket(&packet.CraftingData{Recipes: recipes, PotionRecipes: potionRecipes, PotionContainerChangeRecipes: potionContainerChange, ClearRecipes: true})
}
// sendArmourTrimData sends the armour trim data.
func (s *Session) sendArmourTrimData() {
var trimPatterns []protocol.TrimPattern
var trimMaterials []protocol.TrimMaterial
for _, t := range item.SmithingTemplates() {
if t == item.TemplateNetheriteUpgrade() {
continue
}
name, _ := item.SmithingTemplate{Template: t}.EncodeItem()
trimPatterns = append(trimPatterns, protocol.TrimPattern{
ItemName: name,
PatternID: t.String(),
})
}
for _, i := range item.ArmourTrimMaterials() {
if material, ok := i.(item.ArmourTrimMaterial); ok {
name, _ := i.EncodeItem()
trimMaterials = append(trimMaterials, protocol.TrimMaterial{
MaterialID: material.TrimMaterial(),
Colour: material.MaterialColour(),
ItemName: name,
})
}
}
s.writePacket(&packet.TrimData{Patterns: trimPatterns, Materials: trimMaterials})
}
// sendInv sends the inventory passed to the client with the window ID.
func (s *Session) sendInv(inv *inventory.Inventory, windowID uint32) {
pk := &packet.InventoryContent{
WindowID: windowID,
Content: make([]protocol.ItemInstance, 0, inv.Size()),
}
for _, i := range inv.Slots() {
pk.Content = append(pk.Content, instanceFromItem(s.br, i))
}
s.writePacket(pk)
}
// sendItem sends the item stack passed to the client with the window ID and slot passed.
func (s *Session) sendItem(item item.Stack, slot int, windowID uint32) {
s.writePacket(&packet.InventorySlot{
WindowID: windowID,
Slot: uint32(slot),
NewItem: instanceFromItem(s.br, item),
})
}
const (
craftingGridSizeSmall = 4
craftingGridSizeLarge = 9
craftingGridSmallOffset = 28
craftingGridLargeOffset = 32
craftingResult = 50
)
// smelter is an interface representing a block used to smelt items.
type smelter interface {
// ResetExperience resets the collected experience of the smelter, and returns the amount of experience that was reset.
ResetExperience() int
}
// invByID attempts to return an inventory by the ID passed. If found, the inventory is returned and the bool
// returned is true.
func (s *Session) invByID(id int32, tx *world.Tx) (*inventory.Inventory, bool) {
switch id {
case protocol.ContainerCraftingInput, protocol.ContainerCreatedOutput, protocol.ContainerCursor:
// UI inventory.
return s.ui, true
case protocol.ContainerHotBar, protocol.ContainerInventory, protocol.ContainerCombinedHotBarAndInventory:
// Hotbar 'inventory', rest of inventory, inventory when container is opened.
return s.inv, true
case protocol.ContainerOffhand:
return s.offHand, true
case protocol.ContainerArmor:
// Armour inventory.
return s.armour.Inventory(), true
default:
if !s.containerOpened.Load() {
return nil, false
}
switch id {
case protocol.ContainerLevelEntity:
return s.openedWindow.Load(), true
case protocol.ContainerBarrel:
if _, barrel := tx.Block(*s.openedPos.Load()).(block.Barrel); barrel {
return s.openedWindow.Load(), true
}
case protocol.ContainerBeaconPayment:
if _, beacon := tx.Block(*s.openedPos.Load()).(block.Beacon); beacon {
return s.ui, true
}
case protocol.ContainerBrewingStandInput, protocol.ContainerBrewingStandResult, protocol.ContainerBrewingStandFuel:
if _, brewingStand := tx.Block(*s.openedPos.Load()).(block.BrewingStand); brewingStand {
return s.openedWindow.Load(), true
}
case protocol.ContainerAnvilInput, protocol.ContainerAnvilMaterial:
if _, anvil := tx.Block(*s.openedPos.Load()).(block.Anvil); anvil {
return s.ui, true
}
case protocol.ContainerSmithingTableTemplate, protocol.ContainerSmithingTableInput, protocol.ContainerSmithingTableMaterial:
if _, smithing := tx.Block(*s.openedPos.Load()).(block.SmithingTable); smithing {
return s.ui, true
}
case protocol.ContainerLoomInput, protocol.ContainerLoomDye, protocol.ContainerLoomMaterial:
if _, loom := tx.Block(*s.openedPos.Load()).(block.Loom); loom {
return s.ui, true
}
case protocol.ContainerStonecutterInput:
if _, ok := tx.Block(*s.openedPos.Load()).(block.Stonecutter); ok {
return s.ui, true
}
case protocol.ContainerGrindstoneInput, protocol.ContainerGrindstoneAdditional:
if _, ok := tx.Block(*s.openedPos.Load()).(block.Grindstone); ok {
return s.ui, true
}
case protocol.ContainerEnchantingInput, protocol.ContainerEnchantingMaterial:
if _, enchanting := tx.Block(*s.openedPos.Load()).(block.EnchantingTable); enchanting {
return s.ui, true
}
case protocol.ContainerFurnaceIngredient, protocol.ContainerFurnaceFuel, protocol.ContainerFurnaceResult,
protocol.ContainerBlastFurnaceIngredient, protocol.ContainerSmokerIngredient:
if _, ok := tx.Block(*s.openedPos.Load()).(smelter); ok {
return s.openedWindow.Load(), true
}
}
}
return nil, false
}
// Disconnect disconnects the client and ultimately closes the session. If the message passed is non-empty,
// it will be shown to the client.
func (s *Session) Disconnect(message string) {
if s != Nop {
_ = s.conn.WritePacket(&packet.Disconnect{
HideDisconnectionScreen: message == "",
Message: message,
})
_ = s.conn.Flush()
}
}
// SendSpeed sends the speed of the player in an UpdateAttributes packet, so that it is updated client-side.
func (s *Session) SendSpeed(speed float64) {
s.writePacket(&packet.UpdateAttributes{
EntityRuntimeID: selfEntityRuntimeID,
Attributes: []protocol.Attribute{{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:movement",
Value: float32(speed),
Max: math.MaxFloat32,
},
DefaultMax: math.MaxFloat32,
Default: 0.1,
}},
})
}
// SendFood ...
func (s *Session) SendFood(food int, saturation, exhaustion float64) {
s.writePacket(&packet.UpdateAttributes{
EntityRuntimeID: selfEntityRuntimeID,
Attributes: []protocol.Attribute{
{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:player.hunger",
Value: float32(food),
Max: 20,
},
DefaultMax: 20,
Default: 20,
},
{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:player.saturation",
Value: float32(saturation),
Max: 20,
},
DefaultMax: 20,
Default: 20,
},
{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:player.exhaustion",
Value: float32(exhaustion),
Max: 5,
},
DefaultMax: 5,
},
},
})
}
// SendDialogue sends an NPC dialogue to the client of the connection. The Submit method of the dialogue is
// called when the client interacts with a button in the dialogue.
func (s *Session) SendDialogue(d dialogue.Dialogue, e world.Entity) {
b, _ := json.Marshal(d)
h := s.handlers[packet.IDNPCRequest].(*NPCRequestHandler)
h.dialogue = d
h.entityRuntimeID = s.entityRuntimeID(e)
metadata := s.parseEntityMetadata(e)
metadata[protocol.EntityDataKeyHasNPC] = uint8(1)
disp := d.Display()
disp.EntityOffset = disp.EntityOffset.Add(entityOffset(e))
display, _ := json.Marshal(map[string]any{"portrait_offsets": disp})
metadata[protocol.EntityDataKeyNPCData] = string(display)
s.writePacket(&packet.SetActorData{
EntityRuntimeID: h.entityRuntimeID,
EntityMetadata: metadata,
})
s.writePacket(&packet.NPCDialogue{
EntityUniqueID: h.entityRuntimeID,
ActionType: packet.NPCDialogueActionOpen,
Dialogue: d.Body(),
SceneName: "default",
NPCName: d.Title(),
ActionJSON: string(b),
})
}
func (s *Session) CloseDialogue() {
h := s.handlers[packet.IDNPCRequest].(*NPCRequestHandler)
if h.entityRuntimeID == 0 {
return
}
s.writePacket(&packet.NPCDialogue{
EntityUniqueID: h.entityRuntimeID,
ActionType: packet.NPCDialogueActionClose,
})
h.entityRuntimeID = 0
}
// SendForm sends a form to the client of the connection. The Submit method of the form is called when the
// client submits the form.
func (s *Session) SendForm(f form.Form) {
b, _ := json.Marshal(f)
h := s.handlers[packet.IDModalFormResponse].(*ModalFormResponseHandler)
id := h.currentID.Add(1)
h.mu.Lock()
if len(h.forms) > 10 {
s.conf.Log.Debug("SendForm: more than 10 active forms: dropping an existing one")
for k := range h.forms {
delete(h.forms, k)
break
}
}
h.forms[id] = f
h.mu.Unlock()
s.writePacket(&packet.ModalFormRequest{
FormID: id,
FormData: b,
})
}
// CloseForm closes any forms that the player currently has open. If the player has no forms open, nothing
// happens.
func (s *Session) CloseForm() {
s.writePacket(&packet.ClientBoundCloseForm{})
}
// Transfer transfers the player to a server with the IP and port passed.
func (s *Session) Transfer(ip net.IP, port int) {
s.writePacket(&packet.Transfer{
Address: ip.String(),
Port: uint16(port),
})
}
// SendGameMode sends the game mode of the Controllable entity of the session to the client. It makes sure the right
// flags are set to create the full game mode.
func (s *Session) SendGameMode(c Controllable) {
if s == Nop {
return
}
s.writePacket(&packet.SetPlayerGameType{GameType: gameTypeFromMode(c.GameMode())})
s.SendAbilities(c)
}
// SendAbilities sends the abilities of the Controllable entity of the session to the client.
func (s *Session) SendAbilities(c Controllable) {
mode, abilities := c.GameMode(), uint32(0)
if mode.AllowsFlying() {
abilities |= protocol.AbilityMayFly
if c.Flying() {
abilities |= protocol.AbilityFlying
}
}
if !mode.HasCollision() {
abilities |= protocol.AbilityNoClip
defer c.StartFlying()
// If the client is currently on the ground and turned to spectator mode, it will be unable to sprint during
// flight. In order to allow this, we force the client to be flying through a MovePlayer packet.
s.ViewEntityTeleport(c, c.Position())
}
if !mode.AllowsTakingDamage() {
abilities |= protocol.AbilityInvulnerable
}
if mode.CreativeInventory() {
abilities |= protocol.AbilityInstantBuild
}
if mode.AllowsEditing() {
abilities |= protocol.AbilityBuild | protocol.AbilityMine
}
if mode.AllowsInteraction() {
abilities |= protocol.AbilityDoorsAndSwitches | protocol.AbilityOpenContainers | protocol.AbilityAttackPlayers | protocol.AbilityAttackMobs
}
s.writePacket(&packet.UpdateAbilities{AbilityData: protocol.AbilityData{
EntityUniqueID: selfEntityRuntimeID,
PlayerPermissions: packet.PermissionLevelMember,
CommandPermissions: protocol.CommandPermissionLevelAny,
Layers: []protocol.AbilityLayer{
{
Type: protocol.AbilityLayerTypeBase,
Abilities: protocol.AbilityCount - 1,
Values: abilities,
FlySpeed: float32(c.FlightSpeed()),
VerticalFlySpeed: float32(c.VerticalFlightSpeed()),
WalkSpeed: protocol.AbilityBaseWalkSpeed,
},
},
}})
}
// SendHealth sends the health and max health to the player.
func (s *Session) SendHealth(health, max, absorption float64) {
s.writePacket(&packet.UpdateAttributes{
EntityRuntimeID: selfEntityRuntimeID,
Attributes: []protocol.Attribute{{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:health",
Value: float32(math.Ceil(health)),
Max: float32(math.Ceil(max)),
},
DefaultMax: 20,
Default: 20,
}, {
AttributeValue: protocol.AttributeValue{
Name: "minecraft:absorption",
Value: float32(math.Ceil(absorption)),
Max: float32(math.MaxFloat32),
},
DefaultMax: float32(math.MaxFloat32),
}},
})
}
// SendEffect sends an effects passed to the player.
func (s *Session) SendEffect(e effect.Effect) {
s.SendEffectRemoval(e.Type())
id, _ := effect.ID(e.Type())
dur := e.Duration() / (time.Second / 20)
if e.Infinite() {
dur = -1
}
s.writePacket(&packet.MobEffect{
EntityRuntimeID: selfEntityRuntimeID,
Operation: packet.MobEffectAdd,
EffectType: int32(id),
Amplifier: int32(e.Level() - 1),
Particles: !e.ParticlesHidden(),
Duration: int32(dur),
Ambient: e.Ambient(),
})
}
// SendEffectRemoval sends the removal of an effect passed.
func (s *Session) SendEffectRemoval(e effect.Type) {
id, ok := effect.ID(e)
if !ok {
panic(fmt.Sprintf("unregistered effect type %T", e))
}
s.writePacket(&packet.MobEffect{
EntityRuntimeID: selfEntityRuntimeID,
Operation: packet.MobEffectRemove,
EffectType: int32(id),
})
}
// SendGameRules sends all the provided game rules to the player. Once sent, they will be immediately updated
// on the client if they are valid.
func (s *Session) sendGameRules(gameRules []protocol.GameRule) {
s.writePacket(&packet.GameRulesChanged{GameRules: gameRules})
}
// EnableCoordinates will either enable or disable coordinates for the player depending on the value given.
func (s *Session) EnableCoordinates(enable bool) {
//noinspection SpellCheckingInspection
s.sendGameRules([]protocol.GameRule{{Name: "showcoordinates", Value: enable}})
}
// EnableInstantRespawn will either enable or disable instant respawn for the player depending on the value given.
func (s *Session) EnableInstantRespawn(enable bool) {
//noinspection SpellCheckingInspection
s.sendGameRules([]protocol.GameRule{{Name: "doimmediaterespawn", Value: enable}})
}
// HandleInventories starts handling the inventories of the Controllable entity of the session. It sends packets when
// slots in the inventory are changed.
func (s *Session) HandleInventories(tx *world.Tx, c Controllable, inv, offHand, enderChest, ui *inventory.Inventory, armour *inventory.Armour, heldSlot *uint32) {
s.inv = inv
s.inv.SlotFunc(s.broadcastInvFunc(tx, c))
s.offHand = offHand
s.offHand.SlotFunc(s.broadcastOffHandFunc(tx, c))
s.enderChest = enderChest
s.enderChest.SlotFunc(s.broadcastEnderChestFunc(tx, c))
s.armour = armour
s.armour.Inventory().SlotFunc(s.broadcastArmourFunc(tx, c))
s.ui = ui
s.ui.SlotFunc(s.uiInventoryFunc(tx, c))
s.heldSlot = heldSlot
}
func (s *Session) broadcastInvFunc(tx *world.Tx, c Controllable) inventory.SlotFunc {
return func(slot int, _, after item.Stack) {
if slot == int(*s.heldSlot) {
for _, viewer := range tx.Viewers(c.Position()) {
viewer.ViewEntityItems(c)
}
}
if !s.inTransaction.Load() {
s.sendItem(after, slot, protocol.WindowIDInventory)
}
}
}
func (s *Session) broadcastEnderChestFunc(tx *world.Tx, _ Controllable) inventory.SlotFunc {
return func(slot int, _, after item.Stack) {
if !s.inTransaction.Load() {
if _, ok := tx.Block(*s.openedPos.Load()).(block.EnderChest); ok {
s.ViewSlotChange(slot, after)
}
}
}
}
func (s *Session) broadcastOffHandFunc(tx *world.Tx, c Controllable) inventory.SlotFunc {
return func(slot int, _, after item.Stack) {
for _, viewer := range tx.Viewers(c.Position()) {
viewer.ViewEntityItems(c)
}
if !s.inTransaction.Load() {
i, _ := s.offHand.Item(0)
s.writePacket(&packet.InventoryContent{
WindowID: protocol.WindowIDOffHand,
Content: []protocol.ItemInstance{instanceFromItem(s.br, i)},
})
}
}
}
func (s *Session) broadcastArmourFunc(tx *world.Tx, c Controllable) inventory.SlotFunc {
return func(slot int, before, after item.Stack) {
inTransaction := s.inTransaction.Load()
if !inTransaction {
s.sendItem(after, slot, protocol.WindowIDArmour)
}
if before.Comparable(after) && before.Empty() == after.Empty() {
// Only send armour if the item type actually changed.
return
}
for _, viewer := range tx.Viewers(c.Position()) {
viewer.ViewEntityArmour(c)
}
if !after.Empty() && inTransaction {
tx.PlaySound(entity.EyePosition(c), sound.EquipItem{Item: after.Item()})
}
}
}
// uiInventoryFunc handles an update to the UI inventory, used for updating enchantment options and possibly more
// in the future.
func (s *Session) uiInventoryFunc(tx *world.Tx, c Controllable) inventory.SlotFunc {
return func(slot int, _, after item.Stack) {
if slot == enchantingInputSlot && s.containerOpened.Load() {
pos := *s.openedPos.Load()
if _, enchanting := tx.Block(pos).(block.EnchantingTable); enchanting {
s.sendEnchantmentOptions(tx, c, pos, after)
}
}
s.sendInv(s.ui, protocol.WindowIDUI)
}
}
// SendHeldSlot sends the currently held hotbar slot.
func (s *Session) SendHeldSlot(slot int, c Controllable, force bool) {
if s.changingSlot.Load() && !force {
return
}
mainHand, _ := c.HeldItems()
s.writePacket(&packet.MobEquipment{
EntityRuntimeID: selfEntityRuntimeID,
NewItem: instanceFromItem(s.br, mainHand),
InventorySlot: byte(slot),
HotBarSlot: byte(slot),
})
}
// VerifyAndSetHeldSlot verifies if the slot passed is a valid hotbar slot and
// if the expected item.Stack is in it. Afterwards, it changes the held slot
// of the player.
func (s *Session) VerifyAndSetHeldSlot(slot int, expected item.Stack, c Controllable) error {
if err := s.VerifySlot(slot, expected); err != nil {
return err
}
s.changingSlot.Store(true)
defer s.changingSlot.Store(false)
return c.SetHeldSlot(slot)
}
// VerifySlot verifies if the slot passed is a valid hotbar slot and if the
// expected item.Stack is in it.
func (s *Session) VerifySlot(slot int, expected item.Stack) error {
// The slot that the player might have selected must be within the hotbar:
// The held item cannot be in a different place in the inventory.
if slot < 0 || slot > 8 {
return fmt.Errorf("slot exceeds hotbar range 0-8: slot is %v", slot)
}
clientSideItem := expected
actual, _ := s.inv.Item(slot)
// The item the client claims to have must be identical to the one we have
// registered server-side.
if !clientSideItem.Equal(actual) {
s.sendItem(actual, slot, protocol.WindowIDInventory)
// Only ever debug these as they are frequent and expected to happen
// whenever client and server get out of sync.
s.conf.Log.Debug("verify slot: client-side item was not equal to server-side item", "client-held", clientSideItem.String(), "server-held", actual.String())
}
return nil
}
// SendExperience sends the experience level and progress from the given experience manager to the player.
func (s *Session) SendExperience(level int, progress float64) {
s.writePacket(&packet.UpdateAttributes{
EntityRuntimeID: selfEntityRuntimeID,
Attributes: []protocol.Attribute{
{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:player.level",
Value: float32(level),
Max: float32(math.MaxInt32),
},
DefaultMax: float32(math.MaxInt32),
},
{
AttributeValue: protocol.AttributeValue{
Name: "minecraft:player.experience",
Value: float32(progress),
Max: 1,
},
DefaultMax: 1,
},
},
})
}
// SendChargeItemComplete sends a packet to indicate that the item charging process has been completed.
func (s *Session) SendChargeItemComplete() {
s.writePacket(&packet.ActorEvent{
EntityRuntimeID: selfEntityRuntimeID,
EventType: packet.ActorEventFinishedChargingItem,
})
}
// ShowHudElement shows a HUD element to the player if it is not already shown. If the element is waiting to
// be hidden, it will be removed from the updates and remain visible to the player.
func (s *Session) ShowHudElement(e hud.Element) {
s.hudMu.Lock()
defer s.hudMu.Unlock()
if _, ok := s.hiddenHud[e]; ok {
s.hudUpdates[e] = true
} else if _, ok = s.hudUpdates[e]; ok {
delete(s.hudUpdates, e)
}
}
// HideHudElement hides a HUD element from the player if it is not already hidden. If the element is waiting
// to be shown, it will be removed from the updates and remain hidden from the player.
func (s *Session) HideHudElement(e hud.Element) {
s.hudMu.Lock()
defer s.hudMu.Unlock()
if _, ok := s.hiddenHud[e]; !ok {
s.hudUpdates[e] = false
} else if _, ok = s.hudUpdates[e]; ok {
delete(s.hudUpdates, e)
}
}
// HudElementHidden checks if a HUD element is currently hidden from the player.
func (s *Session) HudElementHidden(e hud.Element) bool {
s.hudMu.RLock()
defer s.hudMu.RUnlock()
if _, ok := s.hiddenHud[e]; ok {
return true
}
vis, ok := s.hudUpdates[e]
return ok && !vis
}
// SendHudUpdates sends any pending HUD updates to the player. The updates are batched to reduce the number
// of packets being sent. Up to 2 packets will be sent, one for showing elements and one for hiding elements.
func (s *Session) SendHudUpdates() {
s.hudMu.Lock()
if len(s.hudUpdates) == 0 {
s.hudMu.Unlock()
return
}
var show, hide []int32
for e, vis := range s.hudUpdates {
if vis {
show = append(show, int32(e.Uint8()))
delete(s.hiddenHud, e)
} else {
hide = append(hide, int32(e.Uint8()))
s.hiddenHud[e] = struct{}{}
}
}
s.hudUpdates = make(map[hud.Element]bool)
s.hudMu.Unlock()
if len(show) > 0 {
s.writePacket(&packet.SetHud{Elements: show, Visibility: packet.HudVisibilityReset})
}
if len(hide) > 0 {
s.writePacket(&packet.SetHud{Elements: hide, Visibility: packet.HudVisibilityHide})
}
}
// AddDebugShape adds a debug shape to be rendered to the player. If the shape already exists, it will be
// updated with the new information.
func (s *Session) AddDebugShape(shape debug.Shape) {
if s == Nop {
return
}
s.queueDebugShapeUpdate(debugShapeUpdate{id: shape.ShapeID(), shape: shape})
}
// RemoveDebugShape removes a debug shape from the player by its unique identifier.
func (s *Session) RemoveDebugShape(shape debug.Shape) {
if s == Nop {
return
}
s.queueDebugShapeUpdate(debugShapeUpdate{id: shape.ShapeID()})
}
// VisibleDebugShapes returns a slice of all debug shapes that are currently being shown to the player.
func (s *Session) VisibleDebugShapes() []debug.Shape {
s.debugShapesMu.RLock()
defer s.debugShapesMu.RUnlock()
return slices.Collect(maps.Values(s.debugShapes))
}
// RemoveAllDebugShapes removes all rendered debug shapes from the player, as well as any shapes that have
// not yet been rendered.
func (s *Session) RemoveAllDebugShapes() {
if s == Nop {
return
}
s.debugShapesMu.Lock()
defer s.debugShapesMu.Unlock()
s.debugShapeUpdates = s.debugShapeUpdates[:0]
for id := range s.debugShapes {
s.debugShapeUpdates = append(s.debugShapeUpdates, debugShapeUpdate{id: id})
}
}
// SendDebugShapes sends any pending additions/removals of debug shapes to the player. Shapes should be sent
// every tick to allow for batching and time-efficient updates.
func (s *Session) SendDebugShapes(dim world.Dimension) {
s.debugShapesMu.Lock()
updates := s.debugShapeUpdates
if len(updates) == 0 {
s.debugShapesMu.Unlock()
return
}
shapes := make([]protocol.PrimitiveShape, 0, len(updates))
for _, update := range updates {
if update.shape == nil {
delete(s.debugShapes, update.id)
shapes = append(shapes, protocol.PrimitiveShape{
NetworkID: uint64(update.id),
DimensionID: protocol.Option(s.dimensionID(dim)),
ExtraShapeData: &protocol.LastShape{},
})
continue
}
s.debugShapes[update.id] = update.shape
shapes = append(shapes, debugShapeToProtocol(update.shape, dim, s.shapeAttachedEntityRuntimeID(update.shape)))
}
s.debugShapeUpdates = s.debugShapeUpdates[:0]
s.debugShapesMu.Unlock()
s.writePacket(&packet.PrimitiveShapes{Shapes: shapes})
}
// queueDebugShapeUpdate queues a debug shape mutation to be applied the next time debug shapes are sent.
func (s *Session) queueDebugShapeUpdate(update debugShapeUpdate) {
s.debugShapesMu.Lock()
defer s.debugShapesMu.Unlock()
s.debugShapeUpdates = append(s.debugShapeUpdates, update)
}
// valueOrDefault returns the value passed if it is not the zero value of the type T, otherwise it returns
// the default value provided.
func valueOrDefault[T comparable](v, def T) T {
var zero T
if v == zero {
return def
}
return v
}
// stackFromItem converts an item.Stack to its network ItemStack representation.
func stackFromItem(br world.BlockRegistry, it item.Stack) protocol.ItemStack {
if it.Empty() {
return protocol.ItemStack{}
}
var blockRuntimeID uint32
if b, ok := it.Item().(world.Block); ok {
blockRuntimeID = br.BlockRuntimeID(b)
}
rid, meta, _ := world.ItemRuntimeID(it.Item())
return protocol.ItemStack{
ItemType: protocol.ItemType{
NetworkID: rid,
MetadataValue: uint32(meta),
},
HasNetworkID: true,
Count: uint16(it.Count()),
BlockRuntimeID: int32(blockRuntimeID),
NBTData: nbtconv.WriteItem(it, false),
}
}
// stackToItem converts a network ItemStack representation back to an item.Stack.
func stackToItem(br world.BlockRegistry, it protocol.ItemStack) item.Stack {
t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue))
if !ok {
t = block.Air{}
}
if it.BlockRuntimeID > 0 {
// It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID,
// since on the next line, we assert that the block is an item. If it didn't succeed, it'll
// return air anyway.
b, _ := br.BlockByRuntimeID(uint32(it.BlockRuntimeID))
if t, ok = b.(world.Item); !ok {
t = block.Air{}
}
}
//noinspection SpellCheckingInspection
if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 {
t = nbter.DecodeNBT(it.NBTData).(world.Item)
}
s := item.NewStack(t, int(it.Count))
return nbtconv.Item(it.NBTData, &s)
}
// instanceFromItem converts an item.Stack to its network ItemInstance representation.
func instanceFromItem(br world.BlockRegistry, it item.Stack) protocol.ItemInstance {
return protocol.ItemInstance{
StackNetworkID: item_id(it),
Stack: stackFromItem(br, it),
}
}
// stacksToRecipeStacks converts a list of item.Stacks to their protocol representation with damage stripped for recipes.
func stacksToRecipeStacks(br world.BlockRegistry, inputs []item.Stack) []protocol.ItemStack {
items := make([]protocol.ItemStack, 0, len(inputs))
for _, i := range inputs {
items = append(items, deleteDamage(stackFromItem(br, i)))
}
return items
}
// stacksToIngredientItems converts a list of item.Stacks to recipe ingredient items used over the network.
func stacksToIngredientItems(_ world.BlockRegistry, inputs []recipe.Item) []protocol.ItemDescriptorCount {
items := make([]protocol.ItemDescriptorCount, 0, len(inputs))
for _, i := range inputs {
var d protocol.ItemDescriptor = &protocol.InvalidItemDescriptor{}
switch i := i.(type) {
case item.Stack:
if i.Empty() {
items = append(items, protocol.ItemDescriptorCount{Descriptor: &protocol.InvalidItemDescriptor{}})
continue
}
rid, meta, ok := world.ItemRuntimeID(i.Item())
if !ok {
panic("should never happen")
}
if _, ok = i.Value("variants"); ok {
meta = math.MaxInt16 // Used to indicate that the item has multiple selectable variants.
}
d = &protocol.DefaultItemDescriptor{
NetworkID: int16(rid),
MetadataValue: meta,
}
case recipe.ItemTag:
d = &protocol.ItemTagItemDescriptor{Tag: i.Tag()}
}
items = append(items, protocol.ItemDescriptorCount{
Descriptor: d,
Count: int32(i.Count()),
})
}
return items
}
// creativeContent returns all creative groups, and creative inventory items as protocol item stacks.
func creativeContent(br world.BlockRegistry) ([]protocol.CreativeGroup, []protocol.CreativeItem) {
groups := make([]protocol.CreativeGroup, 0, len(creative.Groups()))
for _, group := range creative.Groups() {
groups = append(groups, protocol.CreativeGroup{
Category: int32(group.Category.Uint8()),
Name: group.Name,
Icon: deleteDamage(stackFromItem(br, group.Icon)),
})
}
it := make([]protocol.CreativeItem, 0, len(creative.Items()))
for index, i := range creative.Items() {
group := slices.IndexFunc(creative.Groups(), func(group creative.Group) bool {
return group.Name == i.Group
})
if group < 0 {
continue
}
it = append(it, protocol.CreativeItem{
CreativeItemNetworkID: uint32(index) + 1,
Item: deleteDamage(stackFromItem(br, i.Stack)),
GroupIndex: uint32(group),
})
}
return groups, it
}
// deleteDamage strips the damage from a protocol item.
func deleteDamage(st protocol.ItemStack) protocol.ItemStack {
delete(st.NBTData, "Damage")
return st
}
// protocolToSkin converts protocol.Skin to skin.Skin.
func protocolToSkin(sk protocol.Skin) (s skin.Skin, err error) {
if sk.SkinID == "" {
return skin.Skin{}, fmt.Errorf("SkinID must not be an empty string")
}
s = skin.New(int(sk.SkinImageWidth), int(sk.SkinImageHeight))
s.Persona = sk.PersonaSkin
s.Pix = sk.SkinData
s.Model = sk.SkinGeometry
s.PlayFabID = sk.PlayFabID
s.FullID = sk.FullID
s.Cape = skin.NewCape(int(sk.CapeImageWidth), int(sk.CapeImageHeight))
s.Cape.Pix = sk.CapeData
m := make(map[string]any)
if err = json.Unmarshal(sk.SkinGeometry, &m); err != nil {
return skin.Skin{}, fmt.Errorf("SkinGeometry was not a valid JSON string: %v", err)
}
if s.ModelConfig, err = skin.DecodeModelConfig(sk.SkinResourcePatch); err != nil {
return skin.Skin{}, fmt.Errorf("SkinResourcePatch was not a valid JSON string: %v", err)
}
for _, anim := range sk.Animations {
var t skin.AnimationType
switch anim.AnimationType {
case protocol.SkinAnimationHead:
t = skin.AnimationHead
case protocol.SkinAnimationBody32x32:
t = skin.AnimationBody32x32
case protocol.SkinAnimationBody128x128:
t = skin.AnimationBody128x128
default:
return skin.Skin{}, fmt.Errorf("invalid animation type: %v", anim.AnimationType)
}
animation := skin.NewAnimation(int(anim.ImageWidth), int(anim.ImageHeight), int(anim.ExpressionType), t)
animation.FrameCount = int(anim.FrameCount)
animation.Pix = anim.ImageData
s.Animations = append(s.Animations, animation)
}
return
}
// shapeAttachedEntityRuntimeID returns the runtime ID of the entity attached to a debug shape.
func (s *Session) shapeAttachedEntityRuntimeID(shape debug.Shape) int64 {
var handle *world.EntityHandle
switch shape := shape.(type) {
case *debug.Arrow:
handle = shape.Entity
case *debug.Box:
handle = shape.Entity
case *debug.Circle:
handle = shape.Entity
case *debug.Line:
handle = shape.Entity
case *debug.Sphere:
handle = shape.Entity
case *debug.Text:
handle = shape.Entity
case *debug.Cylinder:
handle = shape.Entity
case *debug.Pyramid:
handle = shape.Entity
case *debug.Ellipsoid:
handle = shape.Entity
case *debug.Cone:
handle = shape.Entity
}
if handle == nil {
return 0
}
return int64(s.handleRuntimeID(handle))
}
// debugShapeToProtocol converts a debug shape to its protocol representation. It also provides defaults
// for some fields such as colour, scale and other per-shape properties.
func debugShapeToProtocol(shape debug.Shape, dim world.Dimension, attachedEntityID int64) protocol.PrimitiveShape {
dimID, _ := world.DimensionID(dim)
ps := protocol.PrimitiveShape{
NetworkID: uint64(shape.ShapeID()),
DimensionID: protocol.Option(int32(dimID)),
}
if attachedEntityID > 0 {
ps.AttachedToEntityID = protocol.Option(attachedEntityID)
}
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
switch shape := shape.(type) {
case *debug.Arrow:
ps.Type = protocol.Option(protocol.PrimitiveShapeArrow)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.ExtraShapeData = &protocol.ArrowShape{
ArrowEndLocation: protocol.Option(vec64To32(shape.EndPosition)),
ArrowHeadLength: protocol.Option(valueOrDefault(float32(shape.HeadLength), 1)),
ArrowHeadRadius: protocol.Option(valueOrDefault(float32(shape.HeadRadius), 0.5)),
Segments: protocol.Option(valueOrDefault(uint8(shape.HeadSegments), 4)),
}
case *debug.Box:
ps.Type = protocol.Option(protocol.PrimitiveShapeBox)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.BoxShape{BoxBound: valueOrDefault(vec64To32(shape.Bounds), mgl32.Vec3{1, 1, 1})}
case *debug.Circle:
ps.Type = protocol.Option(protocol.PrimitiveShapeCircle)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.SphereShape{Segments: valueOrDefault(uint8(shape.Segments), 20)}
case *debug.Line:
ps.Type = protocol.Option(protocol.PrimitiveShapeLine)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.ExtraShapeData = &protocol.LineShape{LineEndLocation: vec64To32(shape.EndPosition)}
case *debug.Sphere:
ps.Type = protocol.Option(protocol.PrimitiveShapeSphere)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.SphereShape{Segments: valueOrDefault(uint8(shape.Segments), 20)}
case *debug.Text:
ps.Type = protocol.Option(protocol.PrimitiveShapeText)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
if shape.LockRotation {
ps.Rotation = protocol.Option(vec64To32(shape.Rotation))
}
textData := &protocol.TextShape{
Text: shape.Text,
UseRotation: shape.LockRotation,
DepthTest: !shape.DisableDepthTest,
ShowBackface: !shape.HideBackface,
ShowBackfaceText: !shape.HideBackfaceText,
}
switch {
case shape.HideBackground:
textData.BackgroundColour = protocol.Option(color.RGBA{})
case shape.BackgroundColour != (color.RGBA{}):
textData.BackgroundColour = protocol.Option(shape.BackgroundColour)
}
ps.ExtraShapeData = textData
case *debug.Cylinder:
ps.Type = protocol.Option(protocol.PrimitiveShapeCylinder)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
base := valueOrDefault(shape.BaseRadius, mgl64.Vec2{1, 1})
top := valueOrDefault(shape.TopRadius, base)
ps.ExtraShapeData = &protocol.CylinderShape{
RadiusX: mgl32.Vec2{float32(base[0]), float32(top[0])},
RadiusZ: mgl32.Vec2{float32(base[1]), float32(top[1])},
Height: valueOrDefault(float32(shape.Height), 1),
NumSegments: valueOrDefault(uint8(shape.Segments), 20),
}
case *debug.Pyramid:
ps.Type = protocol.Option(protocol.PrimitiveShapePyramid)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
pyramid := &protocol.PyramidShape{
Width: valueOrDefault(float32(shape.Width), 1),
Height: valueOrDefault(float32(shape.Height), 1),
}
if shape.Depth != 0 {
pyramid.Depth = protocol.Option(float32(shape.Depth))
}
ps.ExtraShapeData = pyramid
case *debug.Ellipsoid:
ps.Type = protocol.Option(protocol.PrimitiveShapeEllipsoid)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.EllipsoidShape{
Radii: valueOrDefault(vec64To32(shape.Radii), mgl32.Vec3{1, 1, 1}),
SegmentsPerAxis: valueOrDefault(uint8(shape.SegmentsPerAxis), 20),
}
case *debug.Cone:
ps.Type = protocol.Option(protocol.PrimitiveShapeCone)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.ConeShape{
Radii: valueOrDefault(vec2To32(shape.Radii), mgl32.Vec2{1, 1}),
Height: valueOrDefault(float32(shape.Height), 1),
NumSegments: valueOrDefault(uint8(shape.Segments), 20),
}
default:
panic(fmt.Sprintf("unknown debug shape type %T", shape))
}
return ps
}
// gameTypeFromMode returns the game type ID from the game mode passed.
func gameTypeFromMode(mode world.GameMode) int32 {
if mode.AllowsFlying() && mode.CreativeInventory() {
return packet.GameTypeCreative
}
if !mode.Visible() && !mode.HasCollision() {
return packet.GameTypeSurvivalSpectator
}
return packet.GameTypeSurvival
}
// The following functions use the go:linkname directive in order to make sure the item.byID and item.toID
// functions do not need to be exported.
// noinspection ALL
//
//go:linkname item_id github.com/df-mc/dragonfly/server/item.id
func item_id(s item.Stack) int32