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