603 lines
20 KiB
Go
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})
|
|
}
|