package session import ( "bytes" "fmt" "image/color" "math/rand/v2" "strings" "time" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "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/inventory" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/particle" "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/nbt" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) // NetworkEncodeableEntity is a world.EntityType where the save ID and network // ID are not the same. type NetworkEncodeableEntity interface { // NetworkEncodeEntity returns the network type ID of the entity. This is // NOT the save ID. NetworkEncodeEntity() string } // OffsetEntity is a world.EntityType that has an additional offset when sent // over network. This is mostly the case for older entities such as players and // TNT. type OffsetEntity interface { NetworkOffset() float64 } // entityHidden checks if a world.Entity is being explicitly hidden from the Session. func (s *Session) entityHidden(e world.Entity) bool { s.entityMutex.RLock() _, ok := s.hiddenEntities[e.H().UUID()] s.entityMutex.RUnlock() return ok } // ViewEntity ... func (s *Session) ViewEntity(e world.Entity) { if e.H() == s.ent { s.ViewEntityState(e) return } if s.entityHidden(e) { return } var runtimeID uint64 _, controllable := e.(Controllable) s.entityMutex.Lock() if id, ok := s.entityRuntimeIDs[e.H()]; ok && controllable { runtimeID = id } else { s.currentEntityRuntimeID += 1 runtimeID = s.currentEntityRuntimeID s.entityRuntimeIDs[e.H()] = runtimeID s.entities[runtimeID] = e.H() } s.entityMutex.Unlock() yaw, pitch := e.Rotation().Elem() metadata := s.entityMetadata(e) id := e.H().Type().EncodeEntity() switch v := e.(type) { case Controllable: _, actualPlayer := sessions.Lookup(v.UUID()) if !actualPlayer { s.writePacket(&packet.PlayerList{ActionType: packet.PlayerListActionAdd, Entries: []protocol.PlayerListEntry{{ UUID: v.UUID(), EntityUniqueID: int64(runtimeID), Username: v.Name(), Skin: skinToProtocol(v.Skin()), }}}) } s.writePacket(&packet.AddPlayer{ EntityMetadata: metadata, EntityRuntimeID: runtimeID, GameType: gameTypeFromMode(v.GameMode()), HeadYaw: float32(yaw), Pitch: float32(pitch), Position: vec64To32(e.Position()), UUID: v.UUID(), Username: v.Name(), Yaw: float32(yaw), AbilityData: protocol.AbilityData{ EntityUniqueID: int64(runtimeID), Layers: []protocol.AbilityLayer{{ Type: protocol.AbilityLayerTypeBase, Abilities: protocol.AbilityCount - 1, }}, }, }) if !actualPlayer { s.writePacket(&packet.PlayerList{ActionType: packet.PlayerListActionRemove, Entries: []protocol.PlayerListEntry{{ UUID: v.UUID(), }}}) } else { s.ViewSkin(e) } return case *entity.Ent: switch e.H().Type() { case entity.ItemType: s.writePacket(&packet.AddItemActor{ EntityUniqueID: int64(runtimeID), EntityRuntimeID: runtimeID, Item: instanceFromItem(s.br, v.Behaviour().(*entity.ItemBehaviour).Item()), Position: vec64To32(v.Position()), Velocity: vec64To32(v.Velocity()), EntityMetadata: metadata, }) return case entity.TextType: metadata[protocol.EntityDataKeyVariant] = int32(s.br.BlockRuntimeID(block.Air{})) case entity.FallingBlockType: metadata[protocol.EntityDataKeyVariant] = int32(s.br.BlockRuntimeID(v.Behaviour().(*entity.FallingBlockBehaviour).Block())) } } if v, ok := e.H().Type().(NetworkEncodeableEntity); ok { id = v.NetworkEncodeEntity() } var vel mgl64.Vec3 if v, ok := e.(interface{ Velocity() mgl64.Vec3 }); ok { vel = v.Velocity() } s.writePacket(&packet.AddActor{ EntityUniqueID: int64(runtimeID), EntityRuntimeID: runtimeID, EntityType: id, EntityMetadata: metadata, Position: vec64To32(e.Position()), Velocity: vec64To32(vel), Pitch: float32(pitch), Yaw: float32(yaw), HeadYaw: float32(yaw), }) } // ViewEntityGameMode ... func (s *Session) ViewEntityGameMode(e world.Entity) { if s.entityHidden(e) { return } c, ok := e.(Controllable) if !ok { return } s.writePacket(&packet.UpdatePlayerGameType{ GameType: gameTypeFromMode(c.GameMode()), PlayerUniqueID: int64(s.entityRuntimeID(c)), }) } // HideEntity ... func (s *Session) HideEntity(e world.Entity) { if s.entityRuntimeID(e) == selfEntityRuntimeID { return } s.entityMutex.Lock() id, ok := s.entityRuntimeIDs[e.H()] if _, controllable := e.(Controllable); !controllable { delete(s.entityRuntimeIDs, e.H()) delete(s.entities, id) } s.entityMutex.Unlock() if !ok { // The entity was already removed some other way. We don't need to send a packet. return } s.writePacket(&packet.RemoveActor{EntityUniqueID: int64(id)}) } // ViewEntityMovement ... func (s *Session) ViewEntityMovement(e world.Entity, pos mgl64.Vec3, rot cube.Rotation, onGround bool) { id := s.entityRuntimeID(e) if (id == selfEntityRuntimeID && s.moving) || s.entityHidden(e) { return } flags := byte(0) if onGround { flags |= packet.MoveFlagOnGround } s.writePacket(&packet.MoveActorAbsolute{ EntityRuntimeID: id, Position: vec64To32(pos.Add(entityOffset(e))), Rotation: vec64To32(mgl64.Vec3{rot.Pitch(), rot.Yaw(), rot.Yaw()}), Flags: flags, }) } // ViewEntityVelocity ... func (s *Session) ViewEntityVelocity(e world.Entity, velocity mgl64.Vec3) { if s.entityHidden(e) { return } s.writePacket(&packet.SetActorMotion{ EntityRuntimeID: s.entityRuntimeID(e), Velocity: vec64To32(velocity), }) } // entityOffset returns the offset that entities have client-side. func entityOffset(e world.Entity) mgl64.Vec3 { if offset, ok := e.H().Type().(OffsetEntity); ok { return mgl64.Vec3{0, offset.NetworkOffset()} } return mgl64.Vec3{} } // ViewTime ... func (s *Session) ViewTime(time int) { s.writePacket(&packet.SetTime{Time: int32(time)}) } // ViewTimeCycle ... func (s *Session) ViewTimeCycle(doDayLightCycle bool) { s.sendGameRules([]protocol.GameRule{{Name: "dodaylightcycle", Value: doDayLightCycle}}) } // ViewEntityTeleport ... func (s *Session) ViewEntityTeleport(e world.Entity, position mgl64.Vec3) { id := s.entityRuntimeID(e) if s.entityHidden(e) { return } yaw, pitch := e.Rotation().Elem() if id == selfEntityRuntimeID { s.teleportPos.Store(&position) } s.writePacket(&packet.SetActorMotion{EntityRuntimeID: id}) if _, ok := e.(Controllable); ok { s.writePacket(&packet.MovePlayer{ EntityRuntimeID: id, Position: vec64To32(position.Add(entityOffset(e))), Pitch: float32(pitch), Yaw: float32(yaw), HeadYaw: float32(yaw), Mode: packet.MoveModeTeleport, }) return } s.writePacket(&packet.MoveActorAbsolute{ EntityRuntimeID: id, Position: vec64To32(position.Add(entityOffset(e))), Rotation: vec64To32(mgl64.Vec3{pitch, yaw, yaw}), Flags: packet.MoveFlagTeleport, }) } // ViewEntityItems ... func (s *Session) ViewEntityItems(e world.Entity) { runtimeID := s.entityRuntimeID(e) if runtimeID == selfEntityRuntimeID || s.entityHidden(e) { // Don't view the items of the entity if the entity is the Controllable entity of the session. return } c, ok := e.(item.Carrier) if !ok { return } mainHand, offHand := c.HeldItems() // Show the main hand item. s.writePacket(&packet.MobEquipment{ EntityRuntimeID: runtimeID, NewItem: instanceFromItem(s.br, mainHand), }) // Show the off-hand item. s.writePacket(&packet.MobEquipment{ EntityRuntimeID: runtimeID, NewItem: instanceFromItem(s.br, offHand), WindowID: protocol.WindowIDOffHand, }) } // ViewEntityArmour ... func (s *Session) ViewEntityArmour(e world.Entity) { runtimeID := s.entityRuntimeID(e) if runtimeID == selfEntityRuntimeID || s.entityHidden(e) { // Don't view the items of the entity if the entity is the Controllable entity of the session. return } armoured, ok := e.(interface { Armour() *inventory.Armour }) if !ok { return } inv := armoured.Armour() // Show the entity's armour s.writePacket(&packet.MobArmourEquipment{ EntityRuntimeID: runtimeID, Helmet: instanceFromItem(s.br, inv.Helmet()), Chestplate: instanceFromItem(s.br, inv.Chestplate()), Leggings: instanceFromItem(s.br, inv.Leggings()), Boots: instanceFromItem(s.br, inv.Boots()), }) } // ViewItemCooldown ... func (s *Session) ViewItemCooldown(item world.Item, duration time.Duration) { name, _ := item.EncodeItem() s.writePacket(&packet.ClientStartItemCooldown{ Category: strings.Split(name, ":")[1], Duration: int32(duration.Milliseconds() / 50), }) } // ViewSleepingPlayers ... func (s *Session) ViewSleepingPlayers(sleeping, max int) { buf := bytes.NewBuffer(nil) _ = nbt.NewEncoderWithEncoding(buf, nbt.NetworkLittleEndian).Encode(map[string]any{ "ableToSleep": int32(max), "overworldPlayerCount": int32(max), "sleepingPlayerCount": int32(sleeping), }) eventData := buf.Bytes() s.writePacket(&packet.LevelEventGeneric{ EventID: packet.LevelEventSleepingPlayers, SerialisedEventData: eventData[2 : len(eventData)-1], }) } // ViewParticle ... func (s *Session) ViewParticle(pos mgl64.Vec3, p world.Particle) { switch pa := p.(type) { case particle.DragonEggTeleport: xSign, ySign, zSign := 0, 0, 0 if pa.Diff.X() < 0 { xSign = 1 << 24 } if pa.Diff.Y() < 0 { ySign = 1 << 25 } if pa.Diff.Z() < 0 { zSign = 1 << 26 } s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesDragonEgg, Position: vec64To32(pos), EventData: int32((((((abs(pa.Diff.X()) << 16) | (abs(pa.Diff.Y()) << 8)) | abs(pa.Diff.Z())) | xSign) | ySign) | zSign), }) case particle.Note: s.writePacket(&packet.BlockEvent{ EventType: pa.Instrument.Int32(), EventData: int32(pa.Pitch), Position: protocol.BlockPos{int32(pos.X()), int32(pos.Y()), int32(pos.Z())}, }) case particle.HugeExplosion: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesExplosion, Position: vec64To32(pos), }) case particle.BoneMeal: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleCropGrowth, Position: vec64To32(pos), EventData: int32(boolByte(pa.Area)), }) case particle.BlockForceField: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleDenyBlock, Position: vec64To32(pos), }) case particle.BlockBreak: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesDestroyBlock, Position: vec64To32(pos), EventData: int32(s.br.BlockRuntimeID(pa.Block)), }) case particle.PunchBlock: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesCrackBlock, Position: vec64To32(pos), EventData: int32(s.br.BlockRuntimeID(pa.Block)) | (int32(pa.Face) << 24), }) case particle.EndermanTeleport: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesTeleport, Position: vec64To32(pos), }) case particle.Flame: if pa.Colour != (color.RGBA{}) { s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 57, Position: vec64To32(pos), EventData: nbtconv.Int32FromRGBA(pa.Colour), }) return } s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 8, Position: vec64To32(pos), }) case particle.Evaporate: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesEvaporateWater, Position: vec64To32(pos), }) case particle.SnowballPoof: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 15, Position: vec64To32(pos), }) case particle.EggSmash: rid, meta, _ := world.ItemRuntimeID(item.Egg{}) s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 14, EventData: (rid << 16) | int32(meta), Position: vec64To32(pos), }) case particle.Splash: if (pa.Colour == color.RGBA{}) { pa.Colour, _ = effect.ResultingColour(nil) } s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticlesPotionSplash, EventData: (int32(pa.Colour.A) << 24) | (int32(pa.Colour.R) << 16) | (int32(pa.Colour.G) << 8) | int32(pa.Colour.B), Position: vec64To32(pos), }) case particle.Effect: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 34, EventData: (int32(pa.Colour.A) << 24) | (int32(pa.Colour.R) << 16) | (int32(pa.Colour.G) << 8) | int32(pa.Colour.B), Position: vec64To32(pos), }) case particle.EntityFlame: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 19, Position: vec64To32(pos), }) case particle.Dust: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 33, Position: vec64To32(pos), EventData: nbtconv.Int32FromRGBA(pa.Colour), }) case particle.WaterDrip: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 28, Position: vec64To32(pos), }) case particle.LavaDrip: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 29, Position: vec64To32(pos), }) case particle.Lava: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 10, Position: vec64To32(pos), }) case particle.DustPlume: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventParticleLegacyEvent | 88, Position: vec64To32(pos), }) } } // tierToSoundEvent converts an item.ArmourTier to a sound event associated with equipping it. func tierToSoundEvent(tier item.ArmourTier) string { switch tier.(type) { case item.ArmourTierLeather: return packet.SoundEventEquipLeather case item.ArmourTierCopper: return packet.SoundEventEquipCopper case item.ArmourTierGold: return packet.SoundEventEquipGold case item.ArmourTierChain: return packet.SoundEventEquipChain case item.ArmourTierIron: return packet.SoundEventEquipIron case item.ArmourTierDiamond: return packet.SoundEventEquipDiamond case item.ArmourTierNetherite: return packet.SoundEventEquipNetherite } return packet.SoundEventEquipGeneric } // playSound plays a world.Sound at a position, disabling relative volume if set to true. func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) { pk := &packet.LevelSoundEvent{ Position: vec64To32(pos), EntityType: ":", ExtraData: -1, DisableRelativeVolume: disableRelative, } switch so := t.(type) { case sound.EquipItem: switch i := so.Item.(type) { case item.Helmet: pk.SoundType = tierToSoundEvent(i.Tier) case item.Chestplate: pk.SoundType = tierToSoundEvent(i.Tier) case item.Leggings: pk.SoundType = tierToSoundEvent(i.Tier) case item.Boots: pk.SoundType = tierToSoundEvent(i.Tier) case item.Elytra: pk.SoundType = packet.SoundEventEquipElytra default: pk.SoundType = packet.SoundEventEquipGeneric } case sound.Note: pk.SoundType = packet.SoundEventNote pk.ExtraData = (so.Instrument.Int32() << 8) | int32(so.Pitch) case sound.DoorCrash: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundZombieDoorCrash, Position: vec64To32(pos), }) return case sound.Explosion: pk.SoundType = packet.SoundEventExplode case sound.Thunder: pk.SoundType, pk.EntityType = packet.SoundEventThunder, "minecraft:lightning_bolt" case sound.Click: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundClick, Position: vec64To32(pos), }) return case sound.SignWaxed: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventWaxOn, Position: vec64To32(pos), }) case sound.WaxedSignFailedInteraction: pk.SoundType = packet.SoundEventWaxedSignInteractFail case sound.WaxRemoved: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventWaxOff, Position: vec64To32(pos), }) case sound.CopperScraped: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventScrape, Position: vec64To32(pos), }) case sound.Pop: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundInfinityArrowPickup, Position: vec64To32(pos), }) return case sound.Teleport: pk.SoundType = packet.SoundEventTeleport case sound.ItemAdd: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundAddItem, Position: vec64To32(pos), }) return case sound.ItemFrameRemove: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundItemFrameRemoveItem, Position: vec64To32(pos), }) return case sound.ItemFrameRotate: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundItemFrameRotateItem, Position: vec64To32(pos), }) return case sound.GhastWarning: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundGhastWarning, Position: vec64To32(pos), }) return case sound.GhastShoot: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundGhastFireball, Position: vec64To32(pos), }) return case sound.TNT: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundFuse, Position: vec64To32(pos), }) return case sound.FireworkLaunch: pk.SoundType = packet.SoundEventLaunch case sound.FireworkHugeBlast: pk.SoundType = packet.SoundEventLargeBlast case sound.FireworkBlast: pk.SoundType = packet.SoundEventBlast case sound.FireworkTwinkle: pk.SoundType = packet.SoundEventTwinkle case sound.FurnaceCrackle: pk.SoundType = packet.SoundEventFurnaceUse case sound.CampfireCrackle: pk.SoundType = packet.SoundEventCampfireCrackle case sound.BlastFurnaceCrackle: pk.SoundType = packet.SoundEventBlastFurnaceUse case sound.SmokerCrackle: pk.SoundType = packet.SoundEventSmokerUse case sound.PotionBrewed: pk.SoundType = packet.SoundEventPotionBrewed case sound.UseSpyglass: pk.SoundType = packet.SoundEventUseSpyglass case sound.StopUsingSpyglass: pk.SoundType = packet.SoundEventStopUsingSpyglass case sound.GoatHorn: switch so.Horn { case sound.Ponder(): pk.SoundType = packet.SoundEventGoatCall0 case sound.Sing(): pk.SoundType = packet.SoundEventGoatCall1 case sound.Seek(): pk.SoundType = packet.SoundEventGoatCall2 case sound.Feel(): pk.SoundType = packet.SoundEventGoatCall3 case sound.Admire(): pk.SoundType = packet.SoundEventGoatCall4 case sound.Call(): pk.SoundType = packet.SoundEventGoatCall5 case sound.Yearn(): pk.SoundType = packet.SoundEventGoatCall6 case sound.Dream(): pk.SoundType = packet.SoundEventGoatCall7 } case sound.FireExtinguish: pk.SoundType = packet.SoundEventExtinguishFire case sound.Ignite: pk.SoundType = packet.SoundEventIgnite case sound.Burning: pk.SoundType = packet.SoundEventPlayerHurtOnFire case sound.Drowning: pk.SoundType = packet.SoundEventPlayerHurtDrown case sound.Fall: pk.EntityType = "minecraft:player" if so.Distance > 4 { pk.SoundType = packet.SoundEventFallBig break } pk.SoundType = packet.SoundEventFallSmall case sound.Burp: pk.SoundType = packet.SoundEventBurp case sound.DoorOpen: pk.SoundType, pk.ExtraData = packet.SoundEventDoorOpen, int32(s.br.BlockRuntimeID(so.Block)) case sound.DoorClose: pk.SoundType, pk.ExtraData = packet.SoundEventDoorClose, int32(s.br.BlockRuntimeID(so.Block)) case sound.TrapdoorOpen: pk.SoundType, pk.ExtraData = packet.SoundEventTrapdoorOpen, int32(s.br.BlockRuntimeID(so.Block)) case sound.TrapdoorClose: pk.SoundType, pk.ExtraData = packet.SoundEventTrapdoorClose, int32(s.br.BlockRuntimeID(so.Block)) case sound.FenceGateOpen: pk.SoundType, pk.ExtraData = packet.SoundEventFenceGateOpen, int32(s.br.BlockRuntimeID(so.Block)) case sound.FenceGateClose: pk.SoundType, pk.ExtraData = packet.SoundEventFenceGateClose, int32(s.br.BlockRuntimeID(so.Block)) case sound.Deny: pk.SoundType = packet.SoundEventDeny case sound.BlockPlace: pk.SoundType, pk.ExtraData = packet.SoundEventPlace, int32(s.br.BlockRuntimeID(so.Block)) case sound.AnvilLand: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundAnvilLand, Position: vec64To32(pos), }) return case sound.AnvilUse: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundAnvilUsed, Position: vec64To32(pos), }) return case sound.AnvilBreak: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundAnvilBroken, Position: vec64To32(pos), }) return case sound.ChestClose: pk.SoundType = packet.SoundEventChestClosed case sound.ChestOpen: pk.SoundType = packet.SoundEventChestOpen case sound.EnderChestClose: pk.SoundType = packet.SoundEventEnderChestClosed case sound.EnderChestOpen: pk.SoundType = packet.SoundEventEnderChestOpen case sound.BarrelClose: pk.SoundType = packet.SoundEventBarrelClose case sound.BarrelOpen: pk.SoundType = packet.SoundEventBarrelOpen case sound.BlockBreaking: pk.SoundType, pk.ExtraData = packet.SoundEventHit, int32(s.br.BlockRuntimeID(so.Block)) case sound.ItemBreak: pk.SoundType = packet.SoundEventBreak case sound.ItemUseOn: pk.SoundType, pk.ExtraData = packet.SoundEventItemUseOn, int32(s.br.BlockRuntimeID(so.Block)) case sound.Fizz: pk.SoundType = packet.SoundEventFizz case sound.GlassBreak: pk.SoundType = packet.SoundEventGlass case sound.Attack: pk.SoundType, pk.EntityType = packet.SoundEventAttackStrong, "minecraft:player" if !so.Damage { pk.SoundType = packet.SoundEventAttackNoDamage } case sound.BucketFill: if _, water := so.Liquid.(block.Water); water { pk.SoundType = packet.SoundEventBucketFillWater break } pk.SoundType = packet.SoundEventBucketFillLava case sound.BucketEmpty: if _, water := so.Liquid.(block.Water); water { pk.SoundType = packet.SoundEventBucketEmptyWater break } pk.SoundType = packet.SoundEventBucketEmptyLava case sound.BowShoot: pk.SoundType = packet.SoundEventBow case sound.CrossbowLoad: switch so.Stage { case sound.CrossbowLoadingStart: pk.SoundType = packet.SoundEventCrossbowLoadingStart if so.QuickCharge { pk.SoundType = packet.SoundEventCrossbowQuickChargeStart } case sound.CrossbowLoadingMiddle: pk.SoundType = packet.SoundEventCrossbowLoadingMiddle if so.QuickCharge { pk.SoundType = packet.SoundEventCrossbowQuickChargeMiddle } case sound.CrossbowLoadingEnd: pk.SoundType = packet.SoundEventCrossbowLoadingEnd if so.QuickCharge { pk.SoundType = packet.SoundEventCrossbowQuickChargeEnd } default: panic("invalid crossbow loading stage") } case sound.CrossbowShoot: pk.SoundType = packet.SoundEventCrossbowShoot case sound.ArrowHit: pk.SoundType = packet.SoundEventBowHit case sound.ItemThrow: pk.SoundType, pk.EntityType = packet.SoundEventThrow, "minecraft:player" case sound.LevelUp: pk.SoundType, pk.ExtraData = packet.SoundEventLevelUp, 0x10000000 case sound.Experience: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundExperienceOrbPickup, Position: vec64To32(pos), }) return case sound.MusicDiscPlay: switch so.DiscType { case sound.Disc13(): pk.SoundType = packet.SoundEventRecord13 case sound.DiscCat(): pk.SoundType = packet.SoundEventRecordCat case sound.DiscBlocks(): pk.SoundType = packet.SoundEventRecordBlocks case sound.DiscChirp(): pk.SoundType = packet.SoundEventRecordChirp case sound.DiscFar(): pk.SoundType = packet.SoundEventRecordFar case sound.DiscMall(): pk.SoundType = packet.SoundEventRecordMall case sound.DiscMellohi(): pk.SoundType = packet.SoundEventRecordMellohi case sound.DiscStal(): pk.SoundType = packet.SoundEventRecordStal case sound.DiscStrad(): pk.SoundType = packet.SoundEventRecordStrad case sound.DiscWard(): pk.SoundType = packet.SoundEventRecordWard case sound.Disc11(): pk.SoundType = packet.SoundEventRecord11 case sound.DiscWait(): pk.SoundType = packet.SoundEventRecordWait case sound.DiscOtherside(): pk.SoundType = packet.SoundEventRecordOtherside case sound.DiscPigstep(): pk.SoundType = packet.SoundEventRecordPigstep case sound.Disc5(): pk.SoundType = packet.SoundEventRecord5 case sound.DiscRelic(): pk.SoundType = packet.SoundEventRecordRelic case sound.DiscCreator(): pk.SoundType = packet.SoundEventRecordCreator case sound.DiscCreatorMusicBox(): pk.SoundType = packet.SoundEventRecordCreatorMusicBox case sound.DiscPrecipice(): pk.SoundType = packet.SoundEventRecordPrecipice case sound.DiscTears(): pk.SoundType = packet.SoundEventRecordTears case sound.DiscLavaChicken(): pk.SoundType = packet.SoundEventRecordLavaChicken default: panic(fmt.Errorf("disc (%v) does not have sound", so.DiscType.String())) } case sound.MusicDiscEnd: pk.SoundType = packet.SoundEventRecordNull case sound.FireCharge: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundBlazeFireball, Position: vec64To32(pos), }) return case sound.ComposterEmpty: pk.SoundType = packet.SoundEventComposterEmpty case sound.ComposterFill: pk.SoundType = packet.SoundEventComposterFill case sound.ComposterFillLayer: pk.SoundType = packet.SoundEventComposterFillLayer case sound.ComposterReady: pk.SoundType = packet.SoundEventComposterReady case sound.PowerOn: pk.SoundType = packet.SoundEventPowerOn case sound.PowerOff: pk.SoundType = packet.SoundEventPowerOff case sound.LecternBookPlace: pk.SoundType = packet.SoundEventLecternBookPlace case sound.Totem: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundTotemUsed, Position: vec64To32(pos), }) case sound.DecoratedPotInserted: s.writePacket(&packet.PlaySound{ SoundName: "block.decorated_pot.insert", Position: vec64To32(pos), Volume: 1, Pitch: 0.7 + 0.5*float32(so.Progress), }) return case sound.DecoratedPotInsertFailed: pk.SoundType = packet.SoundEventDecoratedPotInsertFail case sound.LightningExplode: s.writePacket(&packet.PlaySound{ SoundName: "ambient.weather.lightning.impact", Position: vec64To32(pos), Volume: 1, Pitch: 0.7, }) case sound.LightningThunder: s.writePacket(&packet.PlaySound{ SoundName: "ambient.weather.thunder", Position: vec64To32(pos), Volume: 1, Pitch: 1.0, }) } s.writePacket(pk) } // PlaySound plays a world.Sound to the client. The volume is not dependent on the distance to the source if it is a // sound of the LevelSoundEvent packet. func (s *Session) PlaySound(t world.Sound, pos mgl64.Vec3) { if s == Nop { return } s.playSound(pos, t, true) } // ViewSound ... func (s *Session) ViewSound(pos mgl64.Vec3, soundType world.Sound) { s.playSound(pos, soundType, false) } // OpenSign ... func (s *Session) OpenSign(pos cube.Pos, frontSide bool) { blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])} s.writePacket(&packet.OpenSign{ Position: blockPos, FrontSide: frontSide, }) } // ViewFurnaceUpdate updates a furnace for the associated session based on previous times. func (s *Session) ViewFurnaceUpdate(prevCookTime, cookTime, prevRemainingFuelTime, remainingFuelTime, prevMaxFuelTime, maxFuelTime time.Duration) { if prevCookTime != cookTime { s.writePacket(&packet.ContainerSetData{ WindowID: byte(s.openedWindowID.Load()), Key: packet.ContainerDataFurnaceTickCount, Value: int32(cookTime.Milliseconds() / 50), }) } if prevRemainingFuelTime != remainingFuelTime { s.writePacket(&packet.ContainerSetData{ WindowID: byte(s.openedWindowID.Load()), Key: packet.ContainerDataFurnaceLitTime, Value: int32(remainingFuelTime.Milliseconds() / 50), }) } if prevMaxFuelTime != maxFuelTime { s.writePacket(&packet.ContainerSetData{ WindowID: byte(s.openedWindowID.Load()), Key: packet.ContainerDataFurnaceLitDuration, Value: int32(maxFuelTime.Milliseconds() / 50), }) } } // ViewBrewingUpdate updates a brewing stand for the associated session based on previous times. func (s *Session) ViewBrewingUpdate(prevBrewTime, brewTime time.Duration, prevFuelAmount, fuelAmount, prevFuelTotal, fuelTotal int32) { if prevBrewTime != brewTime { s.writePacket(&packet.ContainerSetData{ WindowID: byte(s.openedWindowID.Load()), Key: packet.ContainerDataBrewingStandBrewTime, Value: int32(brewTime.Milliseconds() / 50), }) } if prevFuelAmount != fuelAmount { s.writePacket(&packet.ContainerSetData{ WindowID: byte(s.openedWindowID.Load()), Key: packet.ContainerDataBrewingStandFuelAmount, Value: fuelAmount, }) } if prevFuelTotal != fuelTotal { s.writePacket(&packet.ContainerSetData{ WindowID: byte(s.openedWindowID.Load()), Key: packet.ContainerDataBrewingStandFuelTotal, Value: fuelTotal, }) } } // ViewBlockUpdate ... func (s *Session) ViewBlockUpdate(pos cube.Pos, b world.Block, layer int) { blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])} s.writePacket(&packet.UpdateBlock{ Position: blockPos, NewBlockRuntimeID: s.br.BlockRuntimeID(b), Flags: packet.BlockUpdateNetwork, Layer: uint32(layer), }) if v, ok := b.(world.NBTer); ok { if nbtData := v.EncodeNBT(); nbtData != nil { nbtData["x"], nbtData["y"], nbtData["z"] = int32(pos.X()), int32(pos.Y()), int32(pos.Z()) s.writePacket(&packet.BlockActorData{ Position: blockPos, NBTData: nbtData, }) } } } // ViewEntityAction ... func (s *Session) ViewEntityAction(e world.Entity, a world.EntityAction) { switch act := a.(type) { case entity.SwingArmAction: if _, ok := e.(Controllable); ok { if s.entityRuntimeID(e) == selfEntityRuntimeID && s.swingingArm.Load() { return } s.writePacket(&packet.Animate{ ActionType: packet.AnimateActionSwingArm, EntityRuntimeID: s.entityRuntimeID(e), }) return } s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventStartAttacking, }) case entity.HurtAction: s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventHurt, }) case entity.CriticalHitAction: if act.Count <= 0 { act.Count = 55 } s.writePacket(&packet.Animate{ ActionType: packet.AnimateActionCriticalHit, EntityRuntimeID: s.entityRuntimeID(e), Data: float32(act.Count), }) case entity.EnchantedHitAction: if act.Count <= 0 { act.Count = 15 } s.writePacket(&packet.Animate{ ActionType: packet.AnimateActionMagicCriticalHit, EntityRuntimeID: s.entityRuntimeID(e), Data: float32(act.Count), }) case entity.DeathAction: s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventDeath, }) case entity.PickedUpAction: s.writePacket(&packet.TakeItemActor{ ItemEntityRuntimeID: s.entityRuntimeID(e), TakerEntityRuntimeID: s.entityRuntimeID(act.Collector), }) case entity.ArrowShakeAction: s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventShake, EventData: int32(act.Duration.Milliseconds() / 50), }) case entity.FireworkExplosionAction: s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventFireworksExplode, }) case entity.EatAction: if user, ok := e.(item.User); ok { held, _ := user.HeldItems() it := held.Item() if held.Empty() { // This can happen sometimes if the user switches between items very quickly, so just ignore the action. return } if _, ok := it.(item.Consumable); !ok { // Not consumable, refer to the comment above. return } rid, meta, _ := world.ItemRuntimeID(it) s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventFeed, // It's a little weird how the runtime ID is still shifted 16 bits to the left here, given the // runtime ID already includes the meta, but it seems to work. EventData: (rid << 16) | int32(meta), }) } case entity.TotemUseAction: s.writePacket(&packet.ActorEvent{ EntityRuntimeID: s.entityRuntimeID(e), EventType: packet.ActorEventTalismanActivate, }) } } // ViewEntityState ... func (s *Session) ViewEntityState(e world.Entity) { s.writePacket(&packet.SetActorData{ EntityRuntimeID: s.entityRuntimeID(e), EntityMetadata: s.entityMetadata(e), }) } // entityMetadata returns the metadata of an entity as viewed by the session, including any overrides // applied through its ViewLayer. func (s *Session) entityMetadata(e world.Entity) protocol.EntityMetadata { metadata := s.parseEntityMetadata(e) if s.viewLayer == nil { return metadata } if nt, ok := s.viewLayer.NameTag(e); ok { metadata[protocol.EntityDataKeyName] = nt if nt != "" { metadata[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(1) if !metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) { metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) } if !metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) { metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) } } else { metadata[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(0) if metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) { metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) } if metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) { metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) } } } if st, ok := s.viewLayer.ScoreTag(e); ok { metadata[protocol.EntityDataKeyScore] = st } if visibility := s.viewLayer.Visibility(e); visibility.EnforceVisibility() { invisibleFlag := metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) shouldForceVisible := visibility == world.EnforceVisible() && invisibleFlag shouldForceInvisible := visibility == world.EnforceInvisible() && !invisibleFlag if shouldForceVisible { metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) } else if shouldForceInvisible { metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) } } return metadata } // ViewEntityAnimation ... func (s *Session) ViewEntityAnimation(e world.Entity, a world.EntityAnimation) { s.writePacket(&packet.AnimateEntity{ Animation: a.Name(), NextState: a.NextState(), StopCondition: a.StopCondition(), Controller: a.Controller(), EntityRuntimeIDs: []uint64{ s.entityRuntimeID(e), }, }) } // OpenBlockContainer ... func (s *Session) OpenBlockContainer(pos cube.Pos, tx *world.Tx) { if s.containerOpened.Load() && *s.openedPos.Load() == pos { return } s.closeCurrentContainer(tx, false) b := tx.Block(pos) if container, ok := b.(block.Container); ok { s.openNormalContainer(container, pos, tx) return } // We hit a special kind of window like beacons, which are not actually opened server-side. nextID := s.nextWindowID() // The client does not send a ContainerClose packet for lecterns. _, lectern := b.(block.Lectern) if !lectern { s.containerOpened.Store(true) inv := inventory.New(1, nil) s.openedWindow.Store(inv) s.openedPos.Store(&pos) } var containerType byte switch b := b.(type) { case block.CraftingTable: containerType = protocol.ContainerTypeWorkbench case block.EnchantingTable: containerType = protocol.ContainerTypeEnchantment case block.Anvil: containerType = protocol.ContainerTypeAnvil case block.Beacon: containerType = protocol.ContainerTypeBeacon case block.Lectern: containerType = protocol.ContainerTypeLectern case block.Loom: containerType = protocol.ContainerTypeLoom case block.Grindstone: containerType = protocol.ContainerTypeGrindstone case block.Stonecutter: containerType = protocol.ContainerTypeStonecutter case block.SmithingTable: containerType = protocol.ContainerTypeSmithingTable case block.EnderChest: b.AddViewer(tx, pos) s.openedWindow.Store(s.enderChest) defer s.sendInv(s.enderChest, uint32(nextID)) } s.openedContainerID.Store(uint32(containerType)) s.writePacket(&packet.ContainerOpen{ WindowID: nextID, ContainerType: containerType, ContainerPosition: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}, ContainerEntityUniqueID: -1, }) } // openNormalContainer opens a normal container that can hold items in it server-side. func (s *Session) openNormalContainer(b block.Container, pos cube.Pos, tx *world.Tx) { b.AddViewer(s, tx, pos) // Paired chests might update the block here. b = tx.Block(pos).(block.Container) nextID := s.nextWindowID() s.containerOpened.Store(true) inv := b.Inventory(tx, pos) s.openedWindow.Store(inv) s.openedPos.Store(&pos) var containerType byte switch b.(type) { case block.Furnace: containerType = protocol.ContainerTypeFurnace case block.BrewingStand: containerType = protocol.ContainerTypeBrewingStand case block.BlastFurnace: containerType = protocol.ContainerTypeBlastFurnace case block.Smoker: containerType = protocol.ContainerTypeSmoker case block.Hopper: containerType = protocol.ContainerTypeHopper } s.openedContainerID.Store(uint32(containerType)) s.writePacket(&packet.ContainerOpen{ WindowID: nextID, ContainerType: containerType, ContainerPosition: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}, ContainerEntityUniqueID: -1, }) s.sendInv(b.Inventory(tx, pos), uint32(nextID)) } // ViewSlotChange ... func (s *Session) ViewSlotChange(slot int, newItem item.Stack) { if !s.containerOpened.Load() { return } if s.inTransaction.Load() { // Don't send slot changes to the player itself. return } s.writePacket(&packet.InventorySlot{ WindowID: s.openedWindowID.Load(), Slot: uint32(slot), NewItem: instanceFromItem(s.br, newItem), }) } // ViewBlockAction ... func (s *Session) ViewBlockAction(pos cube.Pos, a world.BlockAction) { blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])} switch t := a.(type) { case block.OpenAction: s.writePacket(&packet.BlockEvent{ Position: blockPos, EventType: packet.BlockEventChangeChestState, EventData: 1, }) case block.CloseAction: s.writePacket(&packet.BlockEvent{ Position: blockPos, EventType: packet.BlockEventChangeChestState, }) case block.StartCrackAction: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventStartBlockCracking, Position: vec64To32(pos.Vec3()), EventData: int32(65535 / (t.BreakTime.Seconds() * 20)), }) case block.StopCrackAction: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventStopBlockCracking, Position: vec64To32(pos.Vec3()), EventData: 0, }) case block.ContinueCrackAction: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventUpdateBlockCracking, Position: vec64To32(pos.Vec3()), EventData: int32(65535 / (t.BreakTime.Seconds() * 20)), }) case block.DecoratedPotWobbleAction: nbt := t.DecoratedPot.EncodeNBT() nbt["x"], nbt["y"], nbt["z"] = blockPos.X(), blockPos.Y(), blockPos.Z() nbt["animation"] = boolByte(t.Success) + 1 s.writePacket(&packet.BlockActorData{ Position: blockPos, NBTData: nbt, }) } } // ViewEmote ... func (s *Session) ViewEmote(player world.Entity, emote uuid.UUID) { flags := byte(packet.EmoteFlagServerSide) if s.emoteChatMuted { flags |= packet.EmoteFlagMuteChat } s.writePacket(&packet.Emote{ EntityRuntimeID: s.entityRuntimeID(player), EmoteID: emote.String(), Flags: flags, }) } // ViewSkin ... func (s *Session) ViewSkin(e world.Entity) { if v, ok := e.(Controllable); ok { s.writePacket(&packet.PlayerSkin{ UUID: v.UUID(), Skin: skinToProtocol(v.Skin()), }) } } // ViewWorldSpawn ... func (s *Session) ViewWorldSpawn(pos cube.Pos) { blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])} s.writePacket(&packet.SetSpawnPosition{ SpawnType: packet.SpawnTypeWorld, Position: blockPos, Dimension: packet.DimensionOverworld, SpawnPosition: blockPos, }) } // ViewWeather ... func (s *Session) ViewWeather(raining, thunder bool) { pk := &packet.LevelEvent{ EventType: packet.LevelEventStopRaining, } if raining { pk.EventType, pk.EventData = packet.LevelEventStartRaining, int32(rand.IntN(50000)+10000) } s.writePacket(pk) pk = &packet.LevelEvent{ EventType: packet.LevelEventStopThunderstorm, } if thunder { pk.EventType, pk.EventData = packet.LevelEventStartThunderstorm, int32(rand.IntN(50000)+10000) } s.writePacket(pk) } // ViewEntityWake ... func (s *Session) ViewEntityWake(e world.Entity) { s.writePacket(&packet.Animate{ EntityRuntimeID: s.entityRuntimeID(e), ActionType: packet.AnimateActionStopSleep, }) } // nextWindowID produces the next window ID for a new window. It is an int of 1-99. func (s *Session) nextWindowID() byte { if s.openedWindowID.CompareAndSwap(99, 1) { return 1 } return byte(s.openedWindowID.Add(1)) } // closeWindow closes the container window currently opened. If no window is open, closeWindow will do // nothing. func (s *Session) closeWindow(clientRequested bool) bool { if !s.containerOpened.CompareAndSwap(true, false) { return false } containerType := byte(s.openedContainerID.Load()) windowID := byte(s.openedWindowID.Load()) s.openedContainerID.Store(0) s.openedWindow.Store(inventory.New(1, nil)) if !clientRequested { s.writePacket(&packet.ContainerClose{ WindowID: windowID, ContainerType: containerType, ServerSide: true, }) } return true } // entityRuntimeID returns the runtime ID of the entity passed. // noinspection GoCommentLeadingSpace func (s *Session) entityRuntimeID(e world.Entity) uint64 { return s.handleRuntimeID(e.H()) } func (s *Session) handleRuntimeID(e *world.EntityHandle) uint64 { s.entityMutex.RLock() defer s.entityMutex.RUnlock() if id, ok := s.entityRuntimeIDs[e]; ok { return id } s.conf.Log.Debug("entity runtime ID not found", "UUID", e.UUID().String()) return 0 } // entityFromRuntimeID attempts to return an entity by its runtime ID. False is returned if no entity with the // ID could be found. func (s *Session) entityFromRuntimeID(id uint64) (*world.EntityHandle, bool) { s.entityMutex.RLock() e, ok := s.entities[id] s.entityMutex.RUnlock() return e, ok } // vec32To64 converts a mgl32.Vec3 to a mgl64.Vec3. func vec32To64(vec3 mgl32.Vec3) mgl64.Vec3 { return mgl64.Vec3{float64(vec3[0]), float64(vec3[1]), float64(vec3[2])} } // vec64To32 converts a mgl64.Vec3 to a mgl32.Vec3. func vec64To32(vec3 mgl64.Vec3) mgl32.Vec3 { return mgl32.Vec3{float32(vec3[0]), float32(vec3[1]), float32(vec3[2])} } // vec2To32 converts a mgl64.Vec2 to a mgl32.Vec2. func vec2To32(vec2 mgl64.Vec2) mgl32.Vec2 { return mgl32.Vec2{float32(vec2[0]), float32(vec2[1])} } // blockPosFromProtocol ... func blockPosFromProtocol(pos protocol.BlockPos) cube.Pos { return cube.Pos{int(pos.X()), int(pos.Y()), int(pos.Z())} } // boolByte returns 1 if the bool passed is true, or 0 if it is false. func boolByte(b bool) uint8 { if b { return 1 } return 0 } // abs ... func abs(a int) int { if a < 0 { return -a } return a }