Files

603 lines
20 KiB
Go

package session
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net"
"sync"
"sync/atomic"
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/cmd"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/item/recipe"
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/player/debug"
"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/go-gl/mathgl/mgl64"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/nbt"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
// Session handles incoming packets from connections and sends outgoing packets by providing a thin layer
// of abstraction over direct packets. A Session basically 'controls' an entity.
type Session struct {
conf Config
once, connOnce sync.Once
ent *world.EntityHandle
conn Conn
handlers map[uint32]packetHandler
packets chan packet.Packet
currentScoreboard atomic.Pointer[string]
currentLines atomic.Pointer[[]string]
chunkLoader *world.Loader
chunkRadius, maxChunkRadius int32
emoteChatMuted bool
teleportPos atomic.Pointer[mgl64.Vec3]
entityMutex sync.RWMutex
// currentEntityRuntimeID holds the runtime ID assigned to the last entity. It is incremented for every
// entity spawned to the session.
currentEntityRuntimeID uint64
// entityRuntimeIDs holds the runtime IDs of entities shown to the session.
entityRuntimeIDs map[*world.EntityHandle]uint64
entities map[uint64]*world.EntityHandle
hiddenEntities map[uuid.UUID]struct{}
// heldSlot is the slot in the inventory that the controllable is holding.
heldSlot *uint32
inv, offHand, enderChest, ui *inventory.Inventory
armour *inventory.Armour
// joinSkin is the first skin that the player joined with. It is sent on
// spawn for the player list, but otherwise updated immediately when the
// player is viewed.
joinSkin skin.Skin
breakingPos cube.Pos
inTransaction, containerOpened atomic.Bool
openedWindowID atomic.Uint32
openedContainerID atomic.Uint32
openedWindow atomic.Pointer[inventory.Inventory]
openedPos atomic.Pointer[cube.Pos]
swingingArm atomic.Bool
changingSlot atomic.Bool
changingDimension atomic.Bool
moving bool
lastChunkPos world.ChunkPos
recipes map[uint32]recipe.Recipe
blobMu sync.Mutex
blobs map[uint64][]byte
openChunkTransactions []map[uint64]struct{}
invOpened bool
hudMu sync.RWMutex
hudUpdates map[hud.Element]bool
hiddenHud map[hud.Element]struct{}
debugShapesMu sync.RWMutex
debugShapes map[int]debug.Shape
debugShapeUpdates []debugShapeUpdate
viewLayer *world.ViewLayer
closeBackground chan struct{}
br world.BlockRegistry
}
// debugShapeUpdate represents a pending debug shape mutation. If shape is nil, the update removes the
// debug shape with the matching ID. Updates are applied in order when the session sends debug shapes.
type debugShapeUpdate struct {
id int
shape debug.Shape
}
// Conn represents a connection that packets are read from and written to by a Session. In addition, it holds some
// information on the identity of the Session.
type Conn interface {
io.Closer
// IdentityData returns the login.IdentityData of a Conn. It contains the UUID, XUID and username of the connection.
IdentityData() login.IdentityData
// ClientData returns the login.ClientData of a Conn. This includes less sensitive data of the player like its skin,
// language code and other non-essential information.
ClientData() login.ClientData
// ClientCacheEnabled specifies if the Conn has the client cache, used for caching chunks client-side, enabled or
// not. Some platforms, like the Nintendo Switch, have this disabled at all times.
ClientCacheEnabled() bool
// ChunkRadius returns the chunk radius as requested by the client at the other end of the Conn.
ChunkRadius() int
// Latency returns the current latency measured over the Conn.
Latency() time.Duration
// Flush flushes the packets buffered by the Conn, sending all of them out immediately.
Flush() error
// RemoteAddr returns the remote network address.
RemoteAddr() net.Addr
// ReadPacket reads a packet.Packet from the Conn. An error is returned if a deadline was set that was
// exceeded or if the Conn was closed while awaiting a packet.
ReadPacket() (pk packet.Packet, err error)
// WritePacket writes a packet.Packet to the Conn. An error is returned if the Conn was closed before sending the
// packet.
WritePacket(pk packet.Packet) error
// StartGameContext starts the game for the Conn with a context to cancel it.
StartGameContext(ctx context.Context, data minecraft.GameData) error
}
// Nop represents a no-operation session. It does not do anything when sending a packet to it.
var Nop = &Session{conf: Config{Log: slog.New(slog.DiscardHandler)}}
// selfEntityRuntimeID is the entity runtime (or unique) ID of the controllable that the session holds.
const selfEntityRuntimeID = 1
// errSelfRuntimeID is an error returned during packet handling for fields that refer to the player itself and
// must therefore always be 1.
var errSelfRuntimeID = errors.New("invalid entity runtime ID: runtime ID for self must always be 1")
type Config struct {
Log *slog.Logger
MaxChunkRadius int
EmoteChatMuted bool
JoinMessage, QuitMessage chat.Translation
HandleStop func(*world.Tx, Controllable)
// BlockRegistry overrides the registry used for network serialization. If nil, world.DefaultBlockRegistry is used.
BlockRegistry world.BlockRegistry
}
func (conf Config) New(conn Conn) *Session {
r := conn.ChunkRadius()
if r > conf.MaxChunkRadius {
r = conf.MaxChunkRadius
_ = conn.WritePacket(&packet.ChunkRadiusUpdated{ChunkRadius: int32(r)})
}
if conf.Log == nil {
conf.Log = slog.Default()
}
raddr := "nethernet"
if addr := conn.RemoteAddr(); addr != nil {
raddr = addr.String()
}
conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", raddr)
s := &Session{}
*s = Session{
openChunkTransactions: make([]map[uint64]struct{}, 0, 8),
closeBackground: make(chan struct{}),
handlers: map[uint32]packetHandler{},
packets: make(chan packet.Packet, 256),
entityRuntimeIDs: map[*world.EntityHandle]uint64{},
entities: map[uint64]*world.EntityHandle{},
hiddenEntities: map[uuid.UUID]struct{}{},
blobs: map[uint64][]byte{},
chunkRadius: int32(r),
maxChunkRadius: int32(conf.MaxChunkRadius),
emoteChatMuted: conf.EmoteChatMuted,
conn: conn,
currentEntityRuntimeID: 1,
heldSlot: new(uint32),
recipes: make(map[uint32]recipe.Recipe),
conf: conf,
hudUpdates: make(map[hud.Element]bool),
hiddenHud: make(map[hud.Element]struct{}),
debugShapes: make(map[int]debug.Shape),
debugShapeUpdates: make([]debugShapeUpdate, 0, 256),
}
s.viewLayer = world.NewViewLayer(s)
s.openedWindow.Store(inventory.New(1, nil))
s.openedPos.Store(&cube.Pos{})
var scoreboardName string
var scoreboardLines []string
s.currentScoreboard.Store(&scoreboardName)
s.currentLines.Store(&scoreboardLines)
if conf.BlockRegistry == nil {
s.br = world.DefaultBlockRegistry
} else {
s.br = conf.BlockRegistry
}
s.registerHandlers()
s.sendBiomes()
groups, items := creativeContent(s.br)
s.writePacket(&packet.CreativeContent{Groups: groups, Items: items})
s.sendRecipes()
s.sendArmourTrimData()
s.SendSpeed(0.1)
go func() {
for {
select {
case <-s.closeBackground:
return
case pk := <-s.packets:
_ = conn.WritePacket(pk)
}
}
}()
return s
}
// SetHandle sets the world.EntityHandle of the Session and attaches a skin to
// other players on join.
func (s *Session) SetHandle(handle *world.EntityHandle, skin skin.Skin) {
s.ent = handle
s.entityRuntimeIDs[handle] = selfEntityRuntimeID
s.entities[selfEntityRuntimeID] = handle
s.joinSkin = skin
sessions.Add(s)
}
// Spawn makes the Controllable passed spawn in the world.World.
// The function passed will be called when the session stops running.
func (s *Session) Spawn(c Controllable, tx *world.Tx) {
s.SendHealth(c.Health(), c.MaxHealth(), c.Absorption())
s.SendExperience(c.ExperienceLevel(), c.ExperienceProgress())
s.SendFood(c.Food(), 0, 0)
pos := c.Position()
s.chunkLoader = world.NewLoader(int(s.chunkRadius), tx.World(), s)
s.chunkLoader.Move(tx, pos)
s.writePacket(&packet.NetworkChunkPublisherUpdate{
Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
Radius: uint32(s.chunkRadius) << 4,
})
s.sendAvailableEntities(tx.World())
c.SetGameMode(c.GameMode())
for _, e := range c.Effects() {
s.SendEffect(e)
}
s.ViewEntityState(c)
s.sendInv(s.inv, protocol.WindowIDInventory)
s.sendInv(s.ui, protocol.WindowIDUI)
s.sendInv(s.offHand, protocol.WindowIDOffHand)
s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour)
chat.Global.Subscribe(c)
if !s.conf.JoinMessage.Zero() {
chat.Global.Writet(s.conf.JoinMessage, s.conn.IdentityData().DisplayName)
}
go s.background()
go s.handlePackets()
}
// Close closes the session, which in turn closes the controllable and the connection that the session
// manages. Close ensures the method only runs code on the first call.
func (s *Session) Close(tx *world.Tx, c Controllable) {
s.once.Do(func() {
s.close(tx, c)
})
}
// close closes the session, which in turn closes the controllable and the connection that the session
// manages.
func (s *Session) close(tx *world.Tx, c Controllable) {
c.MoveItemsToInventory()
s.closeCurrentContainer(tx, false)
if s.viewLayer != nil {
_ = s.viewLayer.Close()
}
s.conf.HandleStop(tx, c)
// Clear the inventories so that they no longer hold references to the connection.
_ = s.inv.Close()
_ = s.offHand.Close()
_ = s.armour.Close()
s.chunkLoader.Close(tx)
if !s.conf.QuitMessage.Zero() {
chat.Global.Writet(s.conf.QuitMessage, s.conn.IdentityData().DisplayName)
}
chat.Global.Unsubscribe(c)
// Note: Be aware of where RemoveEntity is called. This must not be done too
// early.
tx.RemoveEntity(c)
_ = s.ent.Close()
// This should always be called last due to the timing of the removal of
// entity runtime IDs.
sessions.Remove(s, c)
s.entityMutex.Lock()
clear(s.entityRuntimeIDs)
clear(s.entities)
s.entityMutex.Unlock()
}
// CloseConnection closes the underlying connection of the session so that the session ends up being closed
// eventually.
func (s *Session) CloseConnection() {
s.connOnce.Do(func() {
_ = s.conn.Close()
close(s.closeBackground)
})
}
// Addr returns the net.Addr of the client.
func (s *Session) Addr() net.Addr {
return s.conn.RemoteAddr()
}
// Latency returns the latency of the connection.
func (s *Session) Latency() time.Duration {
return s.conn.Latency()
}
// ClientData returns the login.ClientData of the underlying *minecraft.Conn.
func (s *Session) ClientData() login.ClientData {
return s.conn.ClientData()
}
// handlePackets continuously handles incoming packets from the connection. It processes them accordingly.
// Once the connection is closed, handlePackets will return.
func (s *Session) handlePackets() {
defer func() {
// First close the Controllable. This might lead to a world change
// (player might be dead while disconnecting, in which case it will
// respawn first).
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
_ = e.(Controllable).Close()
})
// Because the player might no longer be in the same world after
// closing, we create a new transaction
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
s.Close(tx, e.(Controllable))
})
}()
for {
pk, err := s.conn.ReadPacket()
if err != nil {
return
}
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
err = s.handlePacket(pk, tx, e.(Controllable))
})
if err != nil {
s.conf.Log.Debug("process packet: " + err.Error())
return
}
}
}
// background performs background tasks of the Session. This includes chunk sending and automatic command updating.
// background returns when the Session's connection is closed using CloseConnection.
func (s *Session) background() {
var (
r map[string]map[int]cmd.Runnable
enums map[string]cmd.Enum
enumValues map[string][]string
softEnums = make(map[string]struct{})
ok bool
i int
)
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
co := e.(Controllable)
r = s.sendAvailableCommands(co, softEnums)
enums, enumValues = s.enums(co)
})
t := time.NewTicker(time.Second / 20)
defer t.Stop()
for {
select {
case <-t.C:
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
c := e.(Controllable)
if i++; i%20 == 0 {
// Enum resending happens relatively often and frequent updates are more important than with full
// command changes. Those are generally only related to permission changes, which doesn't happen often.
r = s.resendEnums(enums, enumValues, softEnums, r, c)
}
if i%100 == 0 {
// Try to resend commands only every 5 seconds.
if r, ok = s.resendCommands(r, c, softEnums); ok {
enums, enumValues = s.enums(c)
}
}
s.sendChunks(tx, c)
})
case <-s.closeBackground:
return
}
}
}
// sendChunks sends the next up to 4 chunks to the connection. What chunks are loaded depends on the connection of
// the chunk loader and the chunks that were previously loaded.
func (s *Session) sendChunks(tx *world.Tx, c Controllable) {
var worldSwitched bool
if w := tx.World(); s.chunkLoader.World() != w && w != nil {
worldSwitched = true
s.handleWorldSwitch(w, tx, c)
}
pos := c.Position()
s.chunkLoader.Move(tx, pos)
chunkPos := world.ChunkPos{int32(pos[0]) << 4, int32(pos[2]) << 4}
if s.lastChunkPos != chunkPos || worldSwitched {
s.lastChunkPos = chunkPos
s.writePacket(&packet.NetworkChunkPublisherUpdate{
Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
Radius: uint32(s.chunkRadius) << 4,
})
}
s.blobMu.Lock()
const maxChunkTransactions = 8
toLoad := maxChunkTransactions - len(s.openChunkTransactions)
s.blobMu.Unlock()
if toLoad > 4 {
toLoad = 4
}
s.chunkLoader.Load(tx, toLoad)
}
// handleWorldSwitch handles the player of the Session switching worlds.
func (s *Session) handleWorldSwitch(w *world.World, tx *world.Tx, c Controllable) {
if s.conn.ClientCacheEnabled() {
s.blobMu.Lock()
s.blobs = map[uint64][]byte{}
s.openChunkTransactions = nil
s.blobMu.Unlock()
}
dim, _ := world.DimensionID(w.Dimension())
same := w.Dimension() == s.chunkLoader.World().Dimension()
if !same {
s.changeDimension(int32(dim), false, c)
}
s.ViewEntityTeleport(c, c.Position())
s.chunkLoader.ChangeWorld(tx, w)
}
// changeDimension changes the dimension of the client. If silent is set to true, the portal noise will be stopped
// immediately.
func (s *Session) changeDimension(dim int32, silent bool, c Controllable) {
s.changingDimension.Store(true)
h := s.handlers[packet.IDServerBoundLoadingScreen].(*ServerBoundLoadingScreenHandler)
id := h.currentID.Add(1)
h.expectedID.Store(id)
s.writePacket(&packet.ChangeDimension{
Dimension: dim,
Position: vec64To32(c.Position().Add(entityOffset(c))),
LoadingScreenID: protocol.Option(id),
})
s.writePacket(&packet.StopSound{StopAll: silent})
s.writePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn})
// As of v1.19.50, the dimension ack that is meant to be sent by the client is now sent by the server. The client
// still sends the ack, but after the server has sent it. Thanks to Mojang for another groundbreaking change.
s.writePacket(&packet.PlayerAction{
EntityRuntimeID: selfEntityRuntimeID,
ActionType: protocol.PlayerActionDimensionChangeDone,
})
}
// ChangingDimension returns whether the session is currently changing dimension or not.
func (s *Session) ChangingDimension() bool {
return s.changingDimension.Load()
}
// ChunkRadius returns the chunk radius of the session.
func (s *Session) ChunkRadius() int32 {
return s.chunkRadius
}
// handlePacket handles an incoming packet, processing it accordingly. If the packet had invalid data or was
// otherwise not valid in its context, an error is returned.
func (s *Session) handlePacket(pk packet.Packet, tx *world.Tx, c Controllable) (err error) {
handler, ok := s.handlers[pk.ID()]
if !ok {
s.conf.Log.Debug("unhandled packet", "packet", fmt.Sprintf("%T", pk), "data", fmt.Sprintf("%+v", pk)[1:])
return nil
}
if handler == nil {
// A nil handler means it was explicitly unhandled.
return nil
}
if err := handler.Handle(pk, s, tx, c); err != nil {
return fmt.Errorf("%T: %w", pk, err)
}
return nil
}
// registerHandlers registers all packet handlers found in the packetHandler package.
func (s *Session) registerHandlers() {
s.handlers = map[uint32]packetHandler{
packet.IDActorEvent: nil,
packet.IDAdventureSettings: nil, // Deprecated, the client still sends this though.
packet.IDAnimate: nil,
packet.IDAnvilDamage: nil,
packet.IDBlockActorData: &BlockActorDataHandler{},
packet.IDBlockPickRequest: &BlockPickRequestHandler{},
packet.IDBookEdit: &BookEditHandler{},
packet.IDBossEvent: nil,
packet.IDClientCacheBlobStatus: &ClientCacheBlobStatusHandler{},
packet.IDCommandRequest: &CommandRequestHandler{},
packet.IDContainerClose: &ContainerCloseHandler{},
packet.IDEmote: &EmoteHandler{},
packet.IDEmoteList: nil,
packet.IDFilterText: nil,
packet.IDInteract: &InteractHandler{},
packet.IDInventoryTransaction: &InventoryTransactionHandler{},
packet.IDItemStackRequest: &ItemStackRequestHandler{changes: map[byte]map[byte]changeInfo{}, responseChanges: map[int32]map[*inventory.Inventory]map[byte]responseChange{}},
packet.IDLecternUpdate: &LecternUpdateHandler{},
packet.IDMobEquipment: &MobEquipmentHandler{},
packet.IDModalFormResponse: &ModalFormResponseHandler{forms: make(map[uint32]form.Form)},
packet.IDMovePlayer: nil,
packet.IDNPCRequest: &NPCRequestHandler{},
packet.IDPlayerAction: &PlayerActionHandler{},
packet.IDPlayerAuthInput: &PlayerAuthInputHandler{},
packet.IDPlayerSkin: &PlayerSkinHandler{},
packet.IDRequestAbility: &RequestAbilityHandler{},
packet.IDRequestChunkRadius: &RequestChunkRadiusHandler{},
packet.IDRespawn: &RespawnHandler{},
packet.IDSetPlayerInventoryOptions: nil,
packet.IDSubChunkRequest: &SubChunkRequestHandler{},
packet.IDText: &TextHandler{},
packet.IDServerBoundLoadingScreen: &ServerBoundLoadingScreenHandler{},
packet.IDServerBoundDiagnostics: &ServerBoundDiagnosticsHandler{},
}
}
// writePacket writes a packet to the session's connection if it is not Nop.
func (s *Session) writePacket(pk packet.Packet) {
if s == Nop {
return
}
select {
case s.packets <- pk:
case <-s.closeBackground:
}
}
// actorIdentifier represents the structure of an actor identifier sent over the network.
type actorIdentifier struct {
// ID is a unique namespaced identifier for the entity.
ID string `nbt:"id"`
}
// sendAvailableEntities sends all registered entities to the player.
func (s *Session) sendAvailableEntities(w *world.World) {
var identifiers []actorIdentifier
for _, t := range w.EntityRegistry().Types() {
identifiers = append(identifiers, actorIdentifier{ID: t.EncodeEntity()})
}
serialisedEntityData, err := nbt.Marshal(map[string]any{"idlist": identifiers})
if err != nil {
panic("should never happen")
}
s.writePacket(&packet.AvailableActorIdentifiers{SerialisedEntityIdentifiers: serialisedEntityData})
}