github.com/df-mc/dragonfly@v0.9.13/server/session/session.go (about) 1 package session 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "github.com/df-mc/atomic" 8 "github.com/df-mc/dragonfly/server/block" 9 "github.com/df-mc/dragonfly/server/block/cube" 10 "github.com/df-mc/dragonfly/server/internal/sliceutil" 11 "github.com/df-mc/dragonfly/server/item" 12 "github.com/df-mc/dragonfly/server/item/inventory" 13 "github.com/df-mc/dragonfly/server/item/recipe" 14 "github.com/df-mc/dragonfly/server/player/chat" 15 "github.com/df-mc/dragonfly/server/player/form" 16 "github.com/df-mc/dragonfly/server/world" 17 "github.com/go-gl/mathgl/mgl64" 18 "github.com/sandertv/gophertunnel/minecraft" 19 "github.com/sandertv/gophertunnel/minecraft/nbt" 20 "github.com/sandertv/gophertunnel/minecraft/protocol" 21 "github.com/sandertv/gophertunnel/minecraft/protocol/login" 22 "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 23 "github.com/sandertv/gophertunnel/minecraft/text" 24 "io" 25 "net" 26 "sync" 27 "time" 28 ) 29 30 // Session handles incoming packets from connections and sends outgoing packets by providing a thin layer 31 // of abstraction over direct packets. A Session basically 'controls' an entity. 32 type Session struct { 33 log Logger 34 once, connOnce sync.Once 35 36 c Controllable 37 conn Conn 38 handlers map[uint32]packetHandler 39 40 // onStop is called when the session is stopped. The controllable passed is the controllable that the 41 // session controls. 42 onStop func(controllable Controllable) 43 44 currentScoreboard atomic.Value[string] 45 currentLines atomic.Value[[]string] 46 47 chunkLoader *world.Loader 48 chunkRadius, maxChunkRadius int32 49 50 teleportPos atomic.Value[*mgl64.Vec3] 51 52 entityMutex sync.RWMutex 53 // currentEntityRuntimeID holds the runtime ID assigned to the last entity. It is incremented for every 54 // entity spawned to the session. 55 currentEntityRuntimeID uint64 56 // entityRuntimeIDs holds a list of all runtime IDs of entities spawned to the session. 57 entityRuntimeIDs map[world.Entity]uint64 58 entities map[uint64]world.Entity 59 hiddenEntities map[world.Entity]struct{} 60 61 // heldSlot is the slot in the inventory that the controllable is holding. 62 heldSlot *atomic.Uint32 63 inv, offHand, enderChest, ui *inventory.Inventory 64 armour *inventory.Armour 65 66 breakingPos cube.Pos 67 68 inTransaction, containerOpened atomic.Bool 69 openedWindowID atomic.Uint32 70 openedContainerID atomic.Uint32 71 openedWindow atomic.Value[*inventory.Inventory] 72 openedPos atomic.Value[cube.Pos] 73 swingingArm atomic.Bool 74 75 recipes map[uint32]recipe.Recipe 76 77 blobMu sync.Mutex 78 blobs map[uint64][]byte 79 openChunkTransactions []map[uint64]struct{} 80 invOpened bool 81 82 joinMessage, quitMessage string 83 84 closeBackground chan struct{} 85 } 86 87 // Conn represents a connection that packets are read from and written to by a Session. In addition, it holds some 88 // information on the identity of the Session. 89 type Conn interface { 90 io.Closer 91 // IdentityData returns the login.IdentityData of a Conn. It contains the UUID, XUID and username of the connection. 92 IdentityData() login.IdentityData 93 // ClientData returns the login.ClientData of a Conn. This includes less sensitive data of the player like its skin, 94 // language code and other non-essential information. 95 ClientData() login.ClientData 96 // ClientCacheEnabled specifies if the Conn has the client cache, used for caching chunks client-side, enabled or 97 // not. Some platforms, like the Nintendo Switch, have this disabled at all times. 98 ClientCacheEnabled() bool 99 // ChunkRadius returns the chunk radius as requested by the client at the other end of the Conn. 100 ChunkRadius() int 101 // Latency returns the current latency measured over the Conn. 102 Latency() time.Duration 103 // Flush flushes the packets buffered by the Conn, sending all of them out immediately. 104 Flush() error 105 // RemoteAddr returns the remote network address. 106 RemoteAddr() net.Addr 107 // ReadPacket reads a packet.Packet from the Conn. An error is returned if a deadline was set that was 108 // exceeded or if the Conn was closed while awaiting a packet. 109 ReadPacket() (pk packet.Packet, err error) 110 // WritePacket writes a packet.Packet to the Conn. An error is returned if the Conn was closed before sending the 111 // packet. 112 WritePacket(pk packet.Packet) error 113 // StartGameContext starts the game for the Conn with a context to cancel it. 114 StartGameContext(ctx context.Context, data minecraft.GameData) error 115 } 116 117 // Logger is used to write debug messages to. These messages are sent whenever handling of a packet of a client fails. 118 type Logger interface { 119 Errorf(format string, a ...any) 120 Debugf(format string, a ...any) 121 } 122 123 // Nop represents a no-operation session. It does not do anything when sending a packet to it. 124 var Nop = &Session{} 125 126 // session is a slice of all open sessions. It is protected by the sessionMu, which must be locked whenever 127 // accessing the value. 128 var sessions []*Session 129 var sessionMu sync.Mutex 130 131 // selfEntityRuntimeID is the entity runtime (or unique) ID of the controllable that the session holds. 132 const selfEntityRuntimeID = 1 133 134 // errSelfRuntimeID is an error returned during packet handling for fields that refer to the player itself and 135 // must therefore always be 1. 136 var errSelfRuntimeID = errors.New("invalid entity runtime ID: runtime ID for self must always be 1") 137 138 // New returns a new session using a controllable entity. The session will control this entity using the 139 // packets that it receives. 140 // New takes the connection from which to accept packets. It will start handling these packets after a call to 141 // Session.Spawn(). 142 func New(conn Conn, maxChunkRadius int, log Logger, joinMessage, quitMessage string) *Session { 143 r := conn.ChunkRadius() 144 if r > maxChunkRadius { 145 r = maxChunkRadius 146 _ = conn.WritePacket(&packet.ChunkRadiusUpdated{ChunkRadius: int32(r)}) 147 } 148 149 s := &Session{} 150 *s = Session{ 151 openChunkTransactions: make([]map[uint64]struct{}, 0, 8), 152 closeBackground: make(chan struct{}), 153 ui: inventory.New(53, s.handleInterfaceUpdate), 154 handlers: map[uint32]packetHandler{}, 155 entityRuntimeIDs: map[world.Entity]uint64{}, 156 entities: map[uint64]world.Entity{}, 157 hiddenEntities: map[world.Entity]struct{}{}, 158 blobs: map[uint64][]byte{}, 159 chunkRadius: int32(r), 160 maxChunkRadius: int32(maxChunkRadius), 161 conn: conn, 162 log: log, 163 currentEntityRuntimeID: 1, 164 heldSlot: atomic.NewUint32(0), 165 joinMessage: joinMessage, 166 quitMessage: quitMessage, 167 openedWindow: *atomic.NewValue(inventory.New(1, nil)), 168 } 169 170 s.registerHandlers() 171 return s 172 } 173 174 // Spawn makes the Controllable passed spawn in the world.World. 175 // The function passed will be called when the session stops running. 176 func (s *Session) Spawn(c Controllable, pos mgl64.Vec3, w *world.World, gm world.GameMode, onStop func(controllable Controllable)) { 177 s.onStop = onStop 178 s.c = c 179 s.recipes = make(map[uint32]recipe.Recipe) 180 s.entityRuntimeIDs[c] = selfEntityRuntimeID 181 s.entities[selfEntityRuntimeID] = c 182 183 s.chunkLoader = world.NewLoader(int(s.chunkRadius), w, s) 184 s.chunkLoader.Move(pos) 185 s.writePacket(&packet.NetworkChunkPublisherUpdate{ 186 Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}, 187 Radius: uint32(s.chunkRadius) << 4, 188 }) 189 190 s.sendAvailableEntities(w) 191 192 s.initPlayerList() 193 194 world_add(c, w) 195 s.c.SetGameMode(gm) 196 s.SendSpeed(0.1) 197 for _, e := range s.c.Effects() { 198 s.SendEffect(e) 199 } 200 201 chat.Global.Subscribe(c) 202 if s.joinMessage != "" { 203 _, _ = fmt.Fprintln(chat.Global, text.Colourf("<yellow>%v</yellow>", fmt.Sprintf(s.joinMessage, s.conn.IdentityData().DisplayName))) 204 } 205 206 s.sendInv(s.inv, protocol.WindowIDInventory) 207 s.sendInv(s.ui, protocol.WindowIDUI) 208 s.sendInv(s.offHand, protocol.WindowIDOffHand) 209 s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour) 210 s.writePacket(&packet.CreativeContent{Items: creativeItems()}) 211 s.sendRecipes() 212 } 213 214 // Start makes the session start handling incoming packets from the client. 215 func (s *Session) Start() { 216 s.c.World().AddEntity(s.c) 217 go s.handlePackets() 218 } 219 220 // Controllable returns the Controllable entity that the Session controls. 221 func (s *Session) Controllable() Controllable { 222 return s.c 223 } 224 225 // Close closes the session, which in turn closes the controllable and the connection that the session 226 // manages. Close ensures the method only runs code on the first call. 227 func (s *Session) Close() error { 228 s.once.Do(s.close) 229 return nil 230 } 231 232 // close closes the session, which in turn closes the controllable and the connection that the session 233 // manages. 234 func (s *Session) close() { 235 _ = s.c.Close() 236 237 // Move UI inventory items to the main inventory. 238 for _, it := range s.ui.Items() { 239 if _, err := s.inv.AddItem(it); err != nil { 240 // We couldn't add the item to the main inventory (probably because it was full), so we drop it instead. 241 s.c.Drop(it) 242 } 243 } 244 245 s.onStop(s.c) 246 247 // Clear the inventories so that they no longer hold references to the connection. 248 _ = s.inv.Close() 249 _ = s.offHand.Close() 250 _ = s.armour.Close() 251 252 s.closeCurrentContainer() 253 _ = s.chunkLoader.Close() 254 s.c.World().RemoveEntity(s.c) 255 256 // This should always be called last due to the timing of the removal of entity runtime IDs. 257 s.closePlayerList() 258 s.entityMutex.Lock() 259 s.entityRuntimeIDs, s.entities = map[world.Entity]uint64{}, map[uint64]world.Entity{} 260 s.entityMutex.Unlock() 261 262 if s.quitMessage != "" { 263 _, _ = fmt.Fprintln(chat.Global, text.Colourf("<yellow>%v</yellow>", fmt.Sprintf(s.quitMessage, s.conn.IdentityData().DisplayName))) 264 } 265 chat.Global.Unsubscribe(s.c) 266 } 267 268 // CloseConnection closes the underlying connection of the session so that the session ends up being closed 269 // eventually. 270 func (s *Session) CloseConnection() { 271 s.connOnce.Do(func() { 272 _ = s.conn.Close() 273 s.closeBackground <- struct{}{} 274 }) 275 } 276 277 // Addr returns the net.Addr of the client. 278 func (s *Session) Addr() net.Addr { 279 return s.conn.RemoteAddr() 280 } 281 282 // Latency returns the latency of the connection. 283 func (s *Session) Latency() time.Duration { 284 return s.conn.Latency() 285 } 286 287 // ClientData returns the login.ClientData of the underlying *minecraft.Conn. 288 func (s *Session) ClientData() login.ClientData { 289 return s.conn.ClientData() 290 } 291 292 // handlePackets continuously handles incoming packets from the connection. It processes them accordingly. 293 // Once the connection is closed, handlePackets will return. 294 func (s *Session) handlePackets() { 295 go s.background() 296 297 defer func() { 298 // If this function ends up panicking, we don't want to call s.Close() as it may cause the entire 299 // server to freeze without printing the actual panic message. 300 // Instead, we check if there is a panic to recover, and just propagate the panic if this does happen 301 // to be the case. 302 if err := recover(); err != nil { 303 panic(err) 304 } 305 _ = s.Close() 306 }() 307 for { 308 pk, err := s.conn.ReadPacket() 309 if err != nil { 310 return 311 } 312 if err := s.handlePacket(pk); err != nil { 313 // An error occurred during the handling of a packet. Print the error and stop handling any more 314 // packets. 315 s.log.Debugf("failed processing packet from %v (%v): %v\n", s.conn.RemoteAddr(), s.c.Name(), err) 316 return 317 } 318 } 319 } 320 321 // background performs background tasks of the Session. This includes chunk sending and automatic command updating. 322 // background returns when the Session's connection is closed using CloseConnection. 323 func (s *Session) background() { 324 var ( 325 t = time.NewTicker(time.Second / 20) 326 r = s.sendAvailableCommands() 327 enums, enumValues = s.enums() 328 ok bool 329 i int 330 ) 331 defer t.Stop() 332 333 for { 334 select { 335 case <-t.C: 336 s.sendChunks() 337 338 if i++; i%20 == 0 { 339 // Enum resending happens relatively often and frequent updates are more important than with full 340 // command changes. Those are generally only related to permission changes, which doesn't happen often. 341 s.resendEnums(enums, enumValues) 342 } 343 if i%100 == 0 { 344 // Try to resend commands only every 5 seconds. 345 if r, ok = s.resendCommands(r); ok { 346 enums, enumValues = s.enums() 347 } 348 } 349 case <-s.closeBackground: 350 return 351 } 352 } 353 } 354 355 // sendChunks sends the next up to 4 chunks to the connection. What chunks are loaded depends on the connection of 356 // the chunk loader and the chunks that were previously loaded. 357 func (s *Session) sendChunks() { 358 pos := s.c.Position() 359 s.chunkLoader.Move(pos) 360 s.writePacket(&packet.NetworkChunkPublisherUpdate{ 361 Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}, 362 Radius: uint32(s.chunkRadius) << 4, 363 }) 364 365 const maxChunkTransactions = 8 366 367 if w := s.c.World(); s.chunkLoader.World() != w && w != nil { 368 s.handleWorldSwitch(w) 369 } 370 371 s.blobMu.Lock() 372 toLoad := maxChunkTransactions - len(s.openChunkTransactions) 373 s.blobMu.Unlock() 374 if toLoad > 4 { 375 toLoad = 4 376 } 377 s.chunkLoader.Load(toLoad) 378 } 379 380 // handleWorldSwitch handles the player of the Session switching worlds. 381 func (s *Session) handleWorldSwitch(w *world.World) { 382 if s.conn.ClientCacheEnabled() { 383 s.blobMu.Lock() 384 // Force out all blobs before changing worlds. This ensures no outdated chunk loading in the new world. 385 resp := &packet.ClientCacheMissResponse{Blobs: make([]protocol.CacheBlob, 0, len(s.blobs))} 386 for h, blob := range s.blobs { 387 resp.Blobs = append(resp.Blobs, protocol.CacheBlob{Hash: h, Payload: blob}) 388 } 389 s.writePacket(resp) 390 391 s.blobs = map[uint64][]byte{} 392 s.openChunkTransactions = nil 393 s.blobMu.Unlock() 394 } 395 396 dim, _ := world.DimensionID(w.Dimension()) 397 same := w.Dimension() == s.chunkLoader.World().Dimension() 398 if !same { 399 s.changeDimension(int32(dim), false) 400 } 401 s.ViewEntityTeleport(s.c, s.c.Position()) 402 s.chunkLoader.ChangeWorld(w) 403 } 404 405 // changeDimension changes the dimension of the client. If silent is set to true, the portal noise will be stopped 406 // immediately. 407 func (s *Session) changeDimension(dim int32, silent bool) { 408 s.writePacket(&packet.ChangeDimension{Dimension: dim, Position: vec64To32(s.c.Position().Add(entityOffset(s.c)))}) 409 s.writePacket(&packet.StopSound{StopAll: silent}) 410 s.writePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn}) 411 412 // 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 413 // still sends the ack, but after the server has sent it. Thanks to Mojang for another groundbreaking change. 414 s.writePacket(&packet.PlayerAction{ 415 EntityRuntimeID: selfEntityRuntimeID, 416 ActionType: protocol.PlayerActionDimensionChangeDone, 417 }) 418 } 419 420 // handlePacket handles an incoming packet, processing it accordingly. If the packet had invalid data or was 421 // otherwise not valid in its context, an error is returned. 422 func (s *Session) handlePacket(pk packet.Packet) error { 423 handler, ok := s.handlers[pk.ID()] 424 if !ok { 425 s.log.Debugf("unhandled packet %T%v from %v\n", pk, fmt.Sprintf("%+v", pk)[1:], s.conn.RemoteAddr()) 426 return nil 427 } 428 if handler == nil { 429 // A nil handler means it was explicitly unhandled. 430 return nil 431 } 432 if err := handler.Handle(pk, s); err != nil { 433 return fmt.Errorf("%T: %w", pk, err) 434 } 435 return nil 436 } 437 438 // registerHandlers registers all packet handlers found in the packetHandler package. 439 func (s *Session) registerHandlers() { 440 s.handlers = map[uint32]packetHandler{ 441 packet.IDActorEvent: nil, 442 packet.IDAdventureSettings: nil, // Deprecated, the client still sends this though. 443 packet.IDAnimate: nil, 444 packet.IDAnvilDamage: nil, 445 packet.IDBlockActorData: &BlockActorDataHandler{}, 446 packet.IDBlockPickRequest: &BlockPickRequestHandler{}, 447 packet.IDBookEdit: &BookEditHandler{}, 448 packet.IDBossEvent: nil, 449 packet.IDClientCacheBlobStatus: &ClientCacheBlobStatusHandler{}, 450 packet.IDCommandRequest: &CommandRequestHandler{}, 451 packet.IDContainerClose: &ContainerCloseHandler{}, 452 packet.IDEmote: &EmoteHandler{}, 453 packet.IDEmoteList: nil, 454 packet.IDFilterText: nil, 455 packet.IDInteract: &InteractHandler{}, 456 packet.IDInventoryTransaction: &InventoryTransactionHandler{}, 457 packet.IDItemFrameDropItem: nil, 458 packet.IDItemStackRequest: &ItemStackRequestHandler{changes: map[byte]map[byte]changeInfo{}, responseChanges: map[int32]map[*inventory.Inventory]map[byte]responseChange{}}, 459 packet.IDLecternUpdate: &LecternUpdateHandler{}, 460 packet.IDMobEquipment: &MobEquipmentHandler{}, 461 packet.IDModalFormResponse: &ModalFormResponseHandler{forms: make(map[uint32]form.Form)}, 462 packet.IDMovePlayer: nil, 463 packet.IDPlayerAction: &PlayerActionHandler{}, 464 packet.IDPlayerAuthInput: &PlayerAuthInputHandler{}, 465 packet.IDPlayerSkin: &PlayerSkinHandler{}, 466 packet.IDRequestAbility: &RequestAbilityHandler{}, 467 packet.IDRequestChunkRadius: &RequestChunkRadiusHandler{}, 468 packet.IDRespawn: &RespawnHandler{}, 469 packet.IDSubChunkRequest: &SubChunkRequestHandler{}, 470 packet.IDText: &TextHandler{}, 471 packet.IDTickSync: nil, 472 } 473 } 474 475 // handleInterfaceUpdate handles an update to the UI inventory, used for updating enchantment options and possibly more 476 // in the future. 477 func (s *Session) handleInterfaceUpdate(slot int, _, item item.Stack) { 478 if slot == enchantingInputSlot && s.containerOpened.Load() { 479 pos := s.openedPos.Load() 480 b := s.c.World().Block(pos) 481 if _, enchanting := b.(block.EnchantingTable); enchanting { 482 s.sendEnchantmentOptions(s.c.World(), pos, item) 483 } 484 } 485 } 486 487 // writePacket writes a packet to the session's connection if it is not Nop. 488 func (s *Session) writePacket(pk packet.Packet) { 489 if s == Nop { 490 return 491 } 492 _ = s.conn.WritePacket(pk) 493 } 494 495 // initPlayerList initialises the player list of the session and sends the session itself to all other 496 // sessions currently open. 497 func (s *Session) initPlayerList() { 498 sessionMu.Lock() 499 sessions = append(sessions, s) 500 for _, session := range sessions { 501 // AddStack the player of the session to all sessions currently open, and add the players of all sessions 502 // currently open to the player list of the new session. 503 session.addToPlayerList(s) 504 if s != session { 505 s.addToPlayerList(session) 506 } 507 } 508 sessionMu.Unlock() 509 } 510 511 // closePlayerList closes the player list of the session and removes the session from the player list of all 512 // other sessions. 513 func (s *Session) closePlayerList() { 514 sessionMu.Lock() 515 for _, session := range sessions { 516 // Remove the player of the session from the player list of all other sessions. 517 session.removeFromPlayerList(s) 518 } 519 sessions = sliceutil.DeleteVal(sessions, s) 520 sessionMu.Unlock() 521 } 522 523 // actorIdentifier represents the structure of an actor identifier sent over the network. 524 type actorIdentifier struct { 525 // ID is a unique namespaced identifier for the entity. 526 ID string `nbt:"id"` 527 } 528 529 // sendAvailableEntities sends all registered entities to the player. 530 func (s *Session) sendAvailableEntities(w *world.World) { 531 var identifiers []actorIdentifier 532 for _, t := range w.EntityRegistry().Types() { 533 identifiers = append(identifiers, actorIdentifier{ID: t.EncodeEntity()}) 534 } 535 serializedEntityData, err := nbt.Marshal(map[string]any{"idlist": identifiers}) 536 if err != nil { 537 panic("should never happen") 538 } 539 s.writePacket(&packet.AvailableActorIdentifiers{SerialisedEntityIdentifiers: serializedEntityData}) 540 }