github.com/df-mc/dragonfly@v0.9.13/server/session/player.go (about) 1 package session 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "github.com/df-mc/atomic" 7 "github.com/df-mc/dragonfly/server/block" 8 "github.com/df-mc/dragonfly/server/entity" 9 "github.com/df-mc/dragonfly/server/entity/effect" 10 "github.com/df-mc/dragonfly/server/internal/nbtconv" 11 "github.com/df-mc/dragonfly/server/item" 12 "github.com/df-mc/dragonfly/server/item/creative" 13 "github.com/df-mc/dragonfly/server/item/inventory" 14 "github.com/df-mc/dragonfly/server/item/recipe" 15 "github.com/df-mc/dragonfly/server/player/form" 16 "github.com/df-mc/dragonfly/server/player/skin" 17 "github.com/df-mc/dragonfly/server/world" 18 "github.com/go-gl/mathgl/mgl64" 19 "github.com/google/uuid" 20 "github.com/sandertv/gophertunnel/minecraft/protocol" 21 "github.com/sandertv/gophertunnel/minecraft/protocol/packet" 22 "math" 23 "net" 24 "time" 25 _ "unsafe" // Imported for compiler directives. 26 ) 27 28 // StopShowingEntity stops showing a world.Entity to the Session. It will be completely invisible until a call to 29 // StartShowingEntity is made. 30 func (s *Session) StopShowingEntity(e world.Entity) { 31 s.HideEntity(e) 32 s.entityMutex.Lock() 33 s.hiddenEntities[e] = struct{}{} 34 s.entityMutex.Unlock() 35 } 36 37 // StartShowingEntity starts showing a world.Entity to the Session that was previously hidden using StopShowingEntity. 38 func (s *Session) StartShowingEntity(e world.Entity) { 39 s.entityMutex.Lock() 40 delete(s.hiddenEntities, e) 41 s.entityMutex.Unlock() 42 s.ViewEntity(e) 43 s.ViewEntityState(e) 44 s.ViewEntityItems(e) 45 s.ViewEntityArmour(e) 46 } 47 48 // closeCurrentContainer closes the container the player might currently have open. 49 func (s *Session) closeCurrentContainer() { 50 if !s.containerOpened.Load() { 51 return 52 } 53 s.closeWindow() 54 55 pos := s.openedPos.Load() 56 w := s.c.World() 57 b := w.Block(pos) 58 if container, ok := b.(block.Container); ok { 59 container.RemoveViewer(s, w, pos) 60 } else if enderChest, ok := b.(block.EnderChest); ok { 61 enderChest.RemoveViewer(w, pos) 62 } 63 } 64 65 // EmptyUIInventory attempts to move all items in the UI inventory to the player's main inventory. If the main inventory 66 // is full, the items are dropped on the ground instead. 67 func (s *Session) EmptyUIInventory() { 68 if s == Nop { 69 return 70 } 71 items := s.ui.Clear() 72 leftover := make([]item.Stack, 0, len(items)) 73 for _, i := range items { 74 if n, err := s.inv.AddItem(i); err != nil { 75 leftover = append(leftover, i.Grow(i.Count()-n)) 76 } 77 } 78 for _, i := range leftover { 79 // We can't put this back in the inventory, so the best option here is to simply get rid of the item if 80 // dropping was cancelled. 81 s.c.Drop(i) 82 } 83 } 84 85 // SendRespawn spawns the Controllable entity of the session client-side in the world, provided it has died. 86 func (s *Session) SendRespawn(pos mgl64.Vec3) { 87 s.writePacket(&packet.Respawn{ 88 Position: vec64To32(pos.Add(entityOffset(s.c))), 89 State: packet.RespawnStateReadyToSpawn, 90 EntityRuntimeID: selfEntityRuntimeID, 91 }) 92 } 93 94 // sendRecipes sends the current crafting recipes to the session. 95 func (s *Session) sendRecipes() { 96 recipes := make([]protocol.Recipe, 0, len(recipe.Recipes())) 97 for index, i := range recipe.Recipes() { 98 networkID := uint32(index) + 1 99 s.recipes[networkID] = i 100 101 switch i := i.(type) { 102 case recipe.Shapeless: 103 recipes = append(recipes, &protocol.ShapelessRecipe{ 104 RecipeID: uuid.New().String(), 105 Priority: int32(i.Priority()), 106 Input: stacksToIngredientItems(i.Input()), 107 Output: stacksToRecipeStacks(i.Output()), 108 Block: i.Block(), 109 RecipeNetworkID: networkID, 110 }) 111 case recipe.Shaped: 112 recipes = append(recipes, &protocol.ShapedRecipe{ 113 RecipeID: uuid.New().String(), 114 Priority: int32(i.Priority()), 115 Width: int32(i.Shape().Width()), 116 Height: int32(i.Shape().Height()), 117 Input: stacksToIngredientItems(i.Input()), 118 Output: stacksToRecipeStacks(i.Output()), 119 Block: i.Block(), 120 RecipeNetworkID: networkID, 121 }) 122 case recipe.Smithing: 123 input, output := stacksToIngredientItems(i.Input()), stacksToRecipeStacks(i.Output()) 124 recipes = append(recipes, &protocol.SmithingTransformRecipe{ 125 RecipeID: uuid.New().String(), 126 Base: input[0], 127 Addition: input[1], 128 Template: input[2], 129 Result: output[0], 130 Block: i.Block(), 131 RecipeNetworkID: networkID, 132 }) 133 } 134 } 135 s.writePacket(&packet.CraftingData{Recipes: recipes, ClearRecipes: true}) 136 } 137 138 // sendInv sends the inventory passed to the client with the window ID. 139 func (s *Session) sendInv(inv *inventory.Inventory, windowID uint32) { 140 pk := &packet.InventoryContent{ 141 WindowID: windowID, 142 Content: make([]protocol.ItemInstance, 0, s.inv.Size()), 143 } 144 for _, i := range inv.Slots() { 145 pk.Content = append(pk.Content, instanceFromItem(i)) 146 } 147 s.writePacket(pk) 148 } 149 150 // sendItem sends the item stack passed to the client with the window ID and slot passed. 151 func (s *Session) sendItem(item item.Stack, slot int, windowID uint32) { 152 s.writePacket(&packet.InventorySlot{ 153 WindowID: windowID, 154 Slot: uint32(slot), 155 NewItem: instanceFromItem(item), 156 }) 157 } 158 159 const ( 160 craftingGridSizeSmall = 4 161 craftingGridSizeLarge = 9 162 craftingGridSmallOffset = 28 163 craftingGridLargeOffset = 32 164 craftingResult = 50 165 ) 166 167 // smelter is an interface representing a block used to smelt items. 168 type smelter interface { 169 // ResetExperience resets the collected experience of the smelter, and returns the amount of experience that was reset. 170 ResetExperience() int 171 } 172 173 // invByID attempts to return an inventory by the ID passed. If found, the inventory is returned and the bool 174 // returned is true. 175 func (s *Session) invByID(id int32) (*inventory.Inventory, bool) { 176 switch id { 177 case protocol.ContainerCraftingInput, protocol.ContainerCreatedOutput, protocol.ContainerCursor: 178 // UI inventory. 179 return s.ui, true 180 case protocol.ContainerHotBar, protocol.ContainerInventory, protocol.ContainerCombinedHotBarAndInventory: 181 // Hotbar 'inventory', rest of inventory, inventory when container is opened. 182 return s.inv, true 183 case protocol.ContainerOffhand: 184 return s.offHand, true 185 case protocol.ContainerArmor: 186 // Armour inventory. 187 return s.armour.Inventory(), true 188 case protocol.ContainerLevelEntity: 189 if s.containerOpened.Load() { 190 b := s.c.World().Block(s.openedPos.Load()) 191 if _, chest := b.(block.Chest); chest { 192 return s.openedWindow.Load(), true 193 } else if _, enderChest := b.(block.EnderChest); enderChest { 194 return s.openedWindow.Load(), true 195 } 196 } 197 case protocol.ContainerBarrel: 198 if s.containerOpened.Load() { 199 if _, barrel := s.c.World().Block(s.openedPos.Load()).(block.Barrel); barrel { 200 return s.openedWindow.Load(), true 201 } 202 } 203 case protocol.ContainerBeaconPayment: 204 if s.containerOpened.Load() { 205 if _, beacon := s.c.World().Block(s.openedPos.Load()).(block.Beacon); beacon { 206 return s.ui, true 207 } 208 } 209 case protocol.ContainerAnvilInput, protocol.ContainerAnvilMaterial: 210 if s.containerOpened.Load() { 211 if _, anvil := s.c.World().Block(s.openedPos.Load()).(block.Anvil); anvil { 212 return s.ui, true 213 } 214 } 215 case protocol.ContainerSmithingTableInput, protocol.ContainerSmithingTableMaterial: 216 if s.containerOpened.Load() { 217 if _, smithing := s.c.World().Block(s.openedPos.Load()).(block.SmithingTable); smithing { 218 return s.ui, true 219 } 220 } 221 case protocol.ContainerLoomInput, protocol.ContainerLoomDye, protocol.ContainerLoomMaterial: 222 if s.containerOpened.Load() { 223 if _, loom := s.c.World().Block(s.openedPos.Load()).(block.Loom); loom { 224 return s.ui, true 225 } 226 } 227 case protocol.ContainerStonecutterInput: 228 if s.containerOpened.Load() { 229 if _, ok := s.c.World().Block(s.openedPos.Load()).(block.Stonecutter); ok { 230 return s.ui, true 231 } 232 } 233 case protocol.ContainerGrindstoneInput, protocol.ContainerGrindstoneAdditional: 234 if s.containerOpened.Load() { 235 if _, ok := s.c.World().Block(s.openedPos.Load()).(block.Grindstone); ok { 236 return s.ui, true 237 } 238 } 239 case protocol.ContainerEnchantingInput, protocol.ContainerEnchantingMaterial: 240 if s.containerOpened.Load() { 241 if _, enchanting := s.c.World().Block(s.openedPos.Load()).(block.EnchantingTable); enchanting { 242 return s.ui, true 243 } 244 } 245 case protocol.ContainerFurnaceIngredient, protocol.ContainerFurnaceFuel, protocol.ContainerFurnaceResult, 246 protocol.ContainerBlastFurnaceIngredient, protocol.ContainerSmokerIngredient: 247 if s.containerOpened.Load() { 248 if _, ok := s.c.World().Block(s.openedPos.Load()).(smelter); ok { 249 return s.openedWindow.Load(), true 250 } 251 } 252 } 253 return nil, false 254 } 255 256 // Disconnect disconnects the client and ultimately closes the session. If the message passed is non-empty, 257 // it will be shown to the client. 258 func (s *Session) Disconnect(message string) { 259 if s != Nop { 260 s.writePacket(&packet.Disconnect{ 261 HideDisconnectionScreen: message == "", 262 Message: message, 263 }) 264 _ = s.conn.Flush() 265 } 266 } 267 268 // SendSpeed sends the speed of the player in an UpdateAttributes packet, so that it is updated client-side. 269 func (s *Session) SendSpeed(speed float64) { 270 s.writePacket(&packet.UpdateAttributes{ 271 EntityRuntimeID: selfEntityRuntimeID, 272 Attributes: []protocol.Attribute{{ 273 AttributeValue: protocol.AttributeValue{ 274 Name: "minecraft:movement", 275 Value: float32(speed), 276 Max: math.MaxFloat32, 277 }, 278 Default: 0.1, 279 }}, 280 }) 281 } 282 283 // SendFood ... 284 func (s *Session) SendFood(food int, saturation, exhaustion float64) { 285 s.writePacket(&packet.UpdateAttributes{ 286 EntityRuntimeID: selfEntityRuntimeID, 287 Attributes: []protocol.Attribute{ 288 { 289 AttributeValue: protocol.AttributeValue{ 290 Name: "minecraft:player.hunger", 291 Value: float32(food), 292 Max: 20, 293 }, 294 Default: 20, 295 }, 296 { 297 AttributeValue: protocol.AttributeValue{ 298 Name: "minecraft:player.saturation", 299 Value: float32(saturation), 300 Max: 20, 301 }, 302 Default: 20, 303 }, 304 { 305 AttributeValue: protocol.AttributeValue{ 306 Name: "minecraft:player.exhaustion", 307 Value: float32(exhaustion), 308 Max: 5, 309 }, 310 }, 311 }, 312 }) 313 } 314 315 // SendForm sends a form to the client of the connection. The Submit method of the form is called when the 316 // client submits the form. 317 func (s *Session) SendForm(f form.Form) { 318 b, _ := json.Marshal(f) 319 320 h := s.handlers[packet.IDModalFormResponse].(*ModalFormResponseHandler) 321 id := h.currentID.Add(1) 322 323 h.mu.Lock() 324 if len(h.forms) > 10 { 325 s.log.Debugf("SendForm %v: more than 10 active forms: dropping an existing one.", s.c.Name()) 326 for k := range h.forms { 327 delete(h.forms, k) 328 break 329 } 330 } 331 h.forms[id] = f 332 h.mu.Unlock() 333 334 s.writePacket(&packet.ModalFormRequest{ 335 FormID: id, 336 FormData: b, 337 }) 338 } 339 340 // Transfer transfers the player to a server with the IP and port passed. 341 func (s *Session) Transfer(ip net.IP, port int) { 342 s.writePacket(&packet.Transfer{ 343 Address: ip.String(), 344 Port: uint16(port), 345 }) 346 } 347 348 // SendGameMode sends the game mode of the Controllable entity of the session to the client. It makes sure the right 349 // flags are set to create the full game mode. 350 func (s *Session) SendGameMode(mode world.GameMode) { 351 if s == Nop { 352 return 353 } 354 s.writePacket(&packet.SetPlayerGameType{GameType: gameTypeFromMode(mode)}) 355 s.sendAbilities() 356 } 357 358 // sendAbilities sends the abilities of the Controllable entity of the session to the client. 359 func (s *Session) sendAbilities() { 360 mode, abilities := s.c.GameMode(), uint32(0) 361 if mode.AllowsFlying() { 362 abilities |= protocol.AbilityMayFly 363 if s.c.Flying() { 364 abilities |= protocol.AbilityFlying 365 } 366 } 367 if !mode.HasCollision() { 368 abilities |= protocol.AbilityNoClip 369 defer s.c.StartFlying() 370 // If the client is currently on the ground and turned to spectator mode, it will be unable to sprint during 371 // flight. In order to allow this, we force the client to be flying through a MovePlayer packet. 372 s.ViewEntityTeleport(s.c, s.c.Position()) 373 } 374 if !mode.AllowsTakingDamage() { 375 abilities |= protocol.AbilityInvulnerable 376 } 377 if mode.CreativeInventory() { 378 abilities |= protocol.AbilityInstantBuild 379 } 380 if mode.AllowsEditing() { 381 abilities |= protocol.AbilityBuild | protocol.AbilityMine 382 } 383 if mode.AllowsInteraction() { 384 abilities |= protocol.AbilityDoorsAndSwitches | protocol.AbilityOpenContainers | protocol.AbilityAttackPlayers | protocol.AbilityAttackMobs 385 } 386 s.writePacket(&packet.UpdateAbilities{AbilityData: protocol.AbilityData{ 387 EntityUniqueID: selfEntityRuntimeID, 388 PlayerPermissions: packet.PermissionLevelMember, 389 CommandPermissions: packet.CommandPermissionLevelNormal, 390 Layers: []protocol.AbilityLayer{ // TODO: Support customization of fly and walk speeds. 391 { 392 Type: protocol.AbilityLayerTypeBase, 393 Abilities: protocol.AbilityCount - 1, 394 Values: abilities, 395 FlySpeed: protocol.AbilityBaseFlySpeed, 396 WalkSpeed: protocol.AbilityBaseWalkSpeed, 397 }, 398 }, 399 }}) 400 } 401 402 // SendHealth sends the health and max health to the player. 403 func (s *Session) SendHealth(health *entity.HealthManager) { 404 s.writePacket(&packet.UpdateAttributes{ 405 EntityRuntimeID: selfEntityRuntimeID, 406 Attributes: []protocol.Attribute{{ 407 AttributeValue: protocol.AttributeValue{ 408 Name: "minecraft:health", 409 Value: float32(math.Ceil(health.Health())), 410 Max: float32(math.Ceil(health.MaxHealth())), 411 }, 412 Default: 20, 413 }}, 414 }) 415 } 416 417 // SendAbsorption sends the absorption value passed to the player. 418 func (s *Session) SendAbsorption(value float64) { 419 s.writePacket(&packet.UpdateAttributes{ 420 EntityRuntimeID: selfEntityRuntimeID, 421 Attributes: []protocol.Attribute{{ 422 AttributeValue: protocol.AttributeValue{ 423 Name: "minecraft:absorption", 424 Value: float32(math.Ceil(value)), 425 Max: float32(math.MaxFloat32), 426 }, 427 }}, 428 }) 429 } 430 431 // SendEffect sends an effects passed to the player. 432 func (s *Session) SendEffect(e effect.Effect) { 433 s.SendEffectRemoval(e.Type()) 434 id, _ := effect.ID(e.Type()) 435 s.writePacket(&packet.MobEffect{ 436 EntityRuntimeID: selfEntityRuntimeID, 437 Operation: packet.MobEffectAdd, 438 EffectType: int32(id), 439 Amplifier: int32(e.Level() - 1), 440 Particles: !e.ParticlesHidden(), 441 Duration: int32(e.Duration() / (time.Second / 20)), 442 }) 443 } 444 445 // SendEffectRemoval sends the removal of an effect passed. 446 func (s *Session) SendEffectRemoval(e effect.Type) { 447 id, ok := effect.ID(e) 448 if !ok { 449 panic(fmt.Sprintf("unregistered effect type %T", e)) 450 } 451 s.writePacket(&packet.MobEffect{ 452 EntityRuntimeID: selfEntityRuntimeID, 453 Operation: packet.MobEffectRemove, 454 EffectType: int32(id), 455 }) 456 } 457 458 // SendGameRules sends all the provided game rules to the player. Once sent, they will be immediately updated 459 // on the client if they are valid. 460 func (s *Session) sendGameRules(gameRules []protocol.GameRule) { 461 s.writePacket(&packet.GameRulesChanged{GameRules: gameRules}) 462 } 463 464 // EnableCoordinates will either enable or disable coordinates for the player depending on the value given. 465 func (s *Session) EnableCoordinates(enable bool) { 466 //noinspection SpellCheckingInspection 467 s.sendGameRules([]protocol.GameRule{{Name: "showcoordinates", Value: enable}}) 468 } 469 470 // EnableInstantRespawn will either enable or disable instant respawn for the player depending on the value given. 471 func (s *Session) EnableInstantRespawn(enable bool) { 472 //noinspection SpellCheckingInspection 473 s.sendGameRules([]protocol.GameRule{{Name: "doimmediaterespawn", Value: enable}}) 474 } 475 476 // addToPlayerList adds the player of a session to the player list of this session. It will be shown in the 477 // in-game pause menu screen. 478 func (s *Session) addToPlayerList(session *Session) { 479 c := session.c 480 481 runtimeID := uint64(1) 482 s.entityMutex.Lock() 483 if session != s { 484 s.currentEntityRuntimeID += 1 485 runtimeID = s.currentEntityRuntimeID 486 } 487 s.entityRuntimeIDs[c] = runtimeID 488 s.entities[runtimeID] = c 489 s.entityMutex.Unlock() 490 491 s.writePacket(&packet.PlayerList{ 492 ActionType: packet.PlayerListActionAdd, 493 Entries: []protocol.PlayerListEntry{{ 494 UUID: c.UUID(), 495 EntityUniqueID: int64(runtimeID), 496 Username: c.Name(), 497 XUID: c.XUID(), 498 Skin: skinToProtocol(c.Skin()), 499 }}, 500 }) 501 } 502 503 // skinToProtocol converts a skin to its protocol representation. 504 func skinToProtocol(s skin.Skin) protocol.Skin { 505 var animations []protocol.SkinAnimation 506 for _, animation := range s.Animations { 507 protocolAnim := protocol.SkinAnimation{ 508 ImageWidth: uint32(animation.Bounds().Max.X), 509 ImageHeight: uint32(animation.Bounds().Max.Y), 510 ImageData: animation.Pix, 511 FrameCount: float32(animation.FrameCount), 512 } 513 switch animation.Type() { 514 case skin.AnimationHead: 515 protocolAnim.AnimationType = protocol.SkinAnimationHead 516 case skin.AnimationBody32x32: 517 protocolAnim.AnimationType = protocol.SkinAnimationBody32x32 518 case skin.AnimationBody128x128: 519 protocolAnim.AnimationType = protocol.SkinAnimationBody128x128 520 } 521 protocolAnim.ExpressionType = uint32(animation.AnimationExpression) 522 animations = append(animations, protocolAnim) 523 } 524 525 return protocol.Skin{ 526 PlayFabID: s.PlayFabID, 527 SkinID: uuid.New().String(), 528 SkinResourcePatch: s.ModelConfig.Encode(), 529 SkinImageWidth: uint32(s.Bounds().Max.X), 530 SkinImageHeight: uint32(s.Bounds().Max.Y), 531 SkinData: s.Pix, 532 CapeImageWidth: uint32(s.Cape.Bounds().Max.X), 533 CapeImageHeight: uint32(s.Cape.Bounds().Max.Y), 534 CapeData: s.Cape.Pix, 535 SkinGeometry: s.Model, 536 PersonaSkin: s.Persona, 537 CapeID: uuid.New().String(), 538 FullID: uuid.New().String(), 539 Animations: animations, 540 Trusted: true, 541 OverrideAppearance: true, 542 } 543 } 544 545 // removeFromPlayerList removes the player of a session from the player list of this session. It will no 546 // longer be shown in the in-game pause menu screen. 547 func (s *Session) removeFromPlayerList(session *Session) { 548 c := session.c 549 550 s.entityMutex.Lock() 551 delete(s.entities, s.entityRuntimeIDs[c]) 552 delete(s.entityRuntimeIDs, c) 553 s.entityMutex.Unlock() 554 555 s.writePacket(&packet.PlayerList{ 556 ActionType: packet.PlayerListActionRemove, 557 Entries: []protocol.PlayerListEntry{{ 558 UUID: c.UUID(), 559 }}, 560 }) 561 } 562 563 // HandleInventories starts handling the inventories of the Controllable entity of the session. It sends packets when 564 // slots in the inventory are changed. 565 func (s *Session) HandleInventories() (inv, offHand, enderChest *inventory.Inventory, armour *inventory.Armour, heldSlot *atomic.Uint32) { 566 s.inv = inventory.New(36, func(slot int, _, item item.Stack) { 567 if s.c == nil { 568 return 569 } 570 if slot == int(s.heldSlot.Load()) { 571 for _, viewer := range s.c.World().Viewers(s.c.Position()) { 572 viewer.ViewEntityItems(s.c) 573 } 574 } 575 if !s.inTransaction.Load() { 576 s.sendItem(item, slot, protocol.WindowIDInventory) 577 } 578 }) 579 s.offHand = inventory.New(1, func(slot int, _, item item.Stack) { 580 if s.c == nil { 581 return 582 } 583 for _, viewer := range s.c.World().Viewers(s.c.Position()) { 584 viewer.ViewEntityItems(s.c) 585 } 586 if !s.inTransaction.Load() { 587 i, _ := s.offHand.Item(0) 588 s.writePacket(&packet.InventoryContent{ 589 WindowID: protocol.WindowIDOffHand, 590 Content: []protocol.ItemInstance{ 591 instanceFromItem(i), 592 }, 593 }) 594 } 595 }) 596 s.enderChest = inventory.New(27, func(slot int, _, item item.Stack) { 597 if s.c == nil { 598 return 599 } 600 if !s.inTransaction.Load() { 601 if _, ok := s.c.World().Block(s.openedPos.Load()).(block.EnderChest); ok { 602 s.ViewSlotChange(slot, item) 603 } 604 } 605 }) 606 s.armour = inventory.NewArmour(func(slot int, before, after item.Stack) { 607 if s.c == nil { 608 return 609 } 610 if !s.inTransaction.Load() { 611 s.sendItem(after, slot, protocol.WindowIDArmour) 612 } 613 if before.Comparable(after) && before.Empty() == after.Empty() { 614 // Only send armour if the item type actually changed. 615 return 616 } 617 for _, viewer := range s.c.World().Viewers(s.c.Position()) { 618 viewer.ViewEntityArmour(s.c) 619 } 620 }) 621 return s.inv, s.offHand, s.enderChest, s.armour, s.heldSlot 622 } 623 624 // SetHeldSlot sets the currently held hotbar slot. 625 func (s *Session) SetHeldSlot(slot int) error { 626 if slot > 8 { 627 return fmt.Errorf("slot exceeds hotbar range 0-8: slot is %v", slot) 628 } 629 630 s.heldSlot.Store(uint32(slot)) 631 632 for _, viewer := range s.c.World().Viewers(s.c.Position()) { 633 viewer.ViewEntityItems(s.c) 634 } 635 636 mainHand, _ := s.c.HeldItems() 637 s.writePacket(&packet.MobEquipment{ 638 EntityRuntimeID: selfEntityRuntimeID, 639 NewItem: instanceFromItem(mainHand), 640 InventorySlot: byte(slot), 641 HotBarSlot: byte(slot), 642 }) 643 return nil 644 } 645 646 // UpdateHeldSlot updates the held slot of the Session to the slot passed. It also verifies that the item in that slot 647 // matches an expected item stack. 648 func (s *Session) UpdateHeldSlot(slot int, expected item.Stack) error { 649 // The slot that the player might have selected must be within the hotbar: The held item cannot be in a 650 // different place in the inventory. 651 if slot > 8 { 652 return fmt.Errorf("new held slot exceeds hotbar range 0-8: slot is %v", slot) 653 } 654 if s.heldSlot.Load() == uint32(slot) { 655 // Old slot was the same as new slot, so don't do anything. 656 return nil 657 } 658 // The user swapped changed held slots so stop using item right away. 659 s.c.ReleaseItem() 660 661 s.heldSlot.Store(uint32(slot)) 662 663 clientSideItem := expected 664 actual, _ := s.inv.Item(slot) 665 666 // The item the client claims to have must be identical to the one we have registered server-side. 667 if !clientSideItem.Equal(actual) { 668 // Only ever debug these as they are frequent and expected to happen whenever client and server get 669 // out of sync. 670 s.log.Debugf("failed processing packet from %v (%v): failed changing held slot: client-side item must be identical to server-side item, but got differences: client: %v vs server: %v", s.conn.RemoteAddr(), s.c.Name(), clientSideItem, actual) 671 } 672 for _, viewer := range s.c.World().Viewers(s.c.Position()) { 673 viewer.ViewEntityItems(s.c) 674 } 675 return nil 676 } 677 678 // SendExperience sends the experience level and progress from the given experience manager to the player. 679 func (s *Session) SendExperience(e *entity.ExperienceManager) { 680 level, progress := e.Level(), e.Progress() 681 s.writePacket(&packet.UpdateAttributes{ 682 EntityRuntimeID: selfEntityRuntimeID, 683 Attributes: []protocol.Attribute{ 684 { 685 AttributeValue: protocol.AttributeValue{ 686 Name: "minecraft:player.level", 687 Value: float32(level), 688 Max: float32(math.MaxInt32), 689 }, 690 }, 691 { 692 AttributeValue: protocol.AttributeValue{ 693 Name: "minecraft:player.experience", 694 Value: float32(progress), 695 Max: 1, 696 }, 697 }, 698 }, 699 }) 700 } 701 702 // stackFromItem converts an item.Stack to its network ItemStack representation. 703 func stackFromItem(it item.Stack) protocol.ItemStack { 704 if it.Empty() { 705 return protocol.ItemStack{} 706 } 707 708 var blockRuntimeID uint32 709 if b, ok := it.Item().(world.Block); ok { 710 blockRuntimeID = world.BlockRuntimeID(b) 711 } 712 713 rid, meta, _ := world.ItemRuntimeID(it.Item()) 714 715 return protocol.ItemStack{ 716 ItemType: protocol.ItemType{ 717 NetworkID: rid, 718 MetadataValue: uint32(meta), 719 }, 720 HasNetworkID: true, 721 Count: uint16(it.Count()), 722 BlockRuntimeID: int32(blockRuntimeID), 723 NBTData: nbtconv.WriteItem(it, false), 724 } 725 } 726 727 // stackToItem converts a network ItemStack representation back to an item.Stack. 728 func stackToItem(it protocol.ItemStack) item.Stack { 729 t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue)) 730 if !ok { 731 t = block.Air{} 732 } 733 if it.BlockRuntimeID > 0 { 734 // It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID, 735 // since on the next line, we assert that the block is an item. If it didn't succeed, it'll 736 // return air anyway. 737 b, _ := world.BlockByRuntimeID(uint32(it.BlockRuntimeID)) 738 if t, ok = b.(world.Item); !ok { 739 t = block.Air{} 740 } 741 } 742 //noinspection SpellCheckingInspection 743 if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 { 744 t = nbter.DecodeNBT(it.NBTData).(world.Item) 745 } 746 s := item.NewStack(t, int(it.Count)) 747 return nbtconv.Item(it.NBTData, &s) 748 } 749 750 // instanceFromItem converts an item.Stack to its network ItemInstance representation. 751 func instanceFromItem(it item.Stack) protocol.ItemInstance { 752 return protocol.ItemInstance{ 753 StackNetworkID: item_id(it), 754 Stack: stackFromItem(it), 755 } 756 } 757 758 // stacksToRecipeStacks converts a list of item.Stacks to their protocol representation with damage stripped for recipes. 759 func stacksToRecipeStacks(inputs []item.Stack) []protocol.ItemStack { 760 items := make([]protocol.ItemStack, 0, len(inputs)) 761 for _, i := range inputs { 762 items = append(items, deleteDamage(stackFromItem(i))) 763 } 764 return items 765 } 766 767 // stacksToIngredientItems converts a list of item.Stacks to recipe ingredient items used over the network. 768 func stacksToIngredientItems(inputs []item.Stack) []protocol.ItemDescriptorCount { 769 items := make([]protocol.ItemDescriptorCount, 0, len(inputs)) 770 for _, i := range inputs { 771 if i.Empty() { 772 items = append(items, protocol.ItemDescriptorCount{Descriptor: &protocol.InvalidItemDescriptor{}}) 773 continue 774 } 775 rid, meta, ok := world.ItemRuntimeID(i.Item()) 776 if !ok { 777 panic("should never happen") 778 } 779 if _, ok = i.Value("variants"); ok { 780 meta = math.MaxInt16 // Used to indicate that the item has multiple selectable variants. 781 } 782 items = append(items, protocol.ItemDescriptorCount{ 783 Descriptor: &protocol.DefaultItemDescriptor{ 784 NetworkID: int16(rid), 785 MetadataValue: meta, 786 }, 787 Count: int32(i.Count()), 788 }) 789 } 790 return items 791 } 792 793 // creativeItems returns all creative inventory items as protocol item stacks. 794 func creativeItems() []protocol.CreativeItem { 795 it := make([]protocol.CreativeItem, 0, len(creative.Items())) 796 for index, i := range creative.Items() { 797 it = append(it, protocol.CreativeItem{ 798 CreativeItemNetworkID: uint32(index) + 1, 799 Item: deleteDamage(stackFromItem(i)), 800 }) 801 } 802 return it 803 } 804 805 // deleteDamage strips the damage from a protocol item. 806 func deleteDamage(st protocol.ItemStack) protocol.ItemStack { 807 delete(st.NBTData, "Damage") 808 return st 809 } 810 811 // protocolToSkin converts protocol.Skin to skin.Skin. 812 func protocolToSkin(sk protocol.Skin) (s skin.Skin, err error) { 813 if sk.SkinID == "" { 814 return skin.Skin{}, fmt.Errorf("SkinID must not be an empty string") 815 } 816 817 s = skin.New(int(sk.SkinImageWidth), int(sk.SkinImageHeight)) 818 s.Persona = sk.PersonaSkin 819 s.Pix = sk.SkinData 820 s.Model = sk.SkinGeometry 821 s.PlayFabID = sk.PlayFabID 822 823 s.Cape = skin.NewCape(int(sk.CapeImageWidth), int(sk.CapeImageHeight)) 824 s.Cape.Pix = sk.CapeData 825 826 m := make(map[string]any) 827 if err = json.Unmarshal(sk.SkinGeometry, &m); err != nil { 828 return skin.Skin{}, fmt.Errorf("SkinGeometry was not a valid JSON string: %v", err) 829 } 830 831 if s.ModelConfig, err = skin.DecodeModelConfig(sk.SkinResourcePatch); err != nil { 832 return skin.Skin{}, fmt.Errorf("SkinResourcePatch was not a valid JSON string: %v", err) 833 } 834 835 for _, anim := range sk.Animations { 836 var t skin.AnimationType 837 switch anim.AnimationType { 838 case protocol.SkinAnimationHead: 839 t = skin.AnimationHead 840 case protocol.SkinAnimationBody32x32: 841 t = skin.AnimationBody32x32 842 case protocol.SkinAnimationBody128x128: 843 t = skin.AnimationBody128x128 844 default: 845 return skin.Skin{}, fmt.Errorf("invalid animation type: %v", anim.AnimationType) 846 } 847 848 animation := skin.NewAnimation(int(anim.ImageWidth), int(anim.ImageHeight), int(anim.ExpressionType), t) 849 animation.FrameCount = int(anim.FrameCount) 850 animation.Pix = anim.ImageData 851 852 s.Animations = append(s.Animations, animation) 853 } 854 return 855 } 856 857 // gameTypeFromMode returns the game type ID from the game mode passed. 858 func gameTypeFromMode(mode world.GameMode) int32 { 859 if mode.AllowsFlying() && mode.CreativeInventory() { 860 return packet.GameTypeCreative 861 } 862 if !mode.Visible() && !mode.HasCollision() { 863 return packet.GameTypeSpectator 864 } 865 return packet.GameTypeSurvival 866 } 867 868 // The following functions use the go:linkname directive in order to make sure the item.byID and item.toID 869 // functions do not need to be exported. 870 871 // noinspection ALL 872 // 873 //go:linkname item_id github.com/df-mc/dragonfly/server/item.id 874 func item_id(s item.Stack) int32 875 876 // noinspection ALL 877 // 878 //go:linkname world_add github.com/df-mc/dragonfly/server/world.add 879 func world_add(e world.Entity, w *world.World)