diff --git a/bot/basic/basic.go b/bot/basic/basic.go new file mode 100644 index 0000000..36791e3 --- /dev/null +++ b/bot/basic/basic.go @@ -0,0 +1,32 @@ +package basic + +import ( + "github.com/Tnze/go-mc/bot" + "github.com/Tnze/go-mc/data/packetid" + pk "github.com/Tnze/go-mc/net/packet" +) + +type Player struct { + c *bot.Client + Settings Settings + + PlayerInfo + WorldInfo +} + +func NewPlayer(c *bot.Client, settings Settings) *Player { + b := &Player{c: c, Settings: settings} + c.Events.AddListener( + bot.PacketHandler{Priority: 0, ID: packetid.Login, F: b.handleJoinGamePacket}, + bot.PacketHandler{Priority: 0, ID: packetid.KeepAliveClientbound, F: b.handleKeepAlivePacket}, + ) + return b +} + +func (p *Player) Respawn() error { + const PerformRespawn = 0 + return p.c.Conn.WritePacket(pk.Marshal( + packetid.ClientCommand, + pk.VarInt(PerformRespawn), + )) +} diff --git a/bot/basic/events.go b/bot/basic/events.go new file mode 100644 index 0000000..6702c48 --- /dev/null +++ b/bot/basic/events.go @@ -0,0 +1,83 @@ +package basic + +import ( + "github.com/google/uuid" + + "github.com/Tnze/go-mc/bot" + "github.com/Tnze/go-mc/chat" + "github.com/Tnze/go-mc/data/packetid" + pk "github.com/Tnze/go-mc/net/packet" +) + +type EventsListener struct { + GameStart func() error + ChatMsg func(c chat.Message, pos byte, uuid uuid.UUID) error + Disconnect func(reason chat.Message) error + HealthChange func(health float32) error + Death func() error +} + +func (e EventsListener) Attach(c *bot.Client) { + c.Events.AddListener( + bot.PacketHandler{Priority: 64, ID: packetid.Login, F: e.onJoinGame}, + bot.PacketHandler{Priority: 64, ID: packetid.ChatClientbound, F: e.onChatMsg}, + bot.PacketHandler{Priority: 64, ID: packetid.Disconnect, F: e.onDisconnect}, + bot.PacketHandler{Priority: 64, ID: packetid.UpdateHealth, F: e.onUpdateHealth}, + ) +} + +func (e *EventsListener) onJoinGame(_ pk.Packet) error { + if e.GameStart != nil { + return e.GameStart() + } + return nil +} + +func (e *EventsListener) onDisconnect(p pk.Packet) error { + if e.Disconnect != nil { + var reason chat.Message + if err := p.Scan(&reason); err != nil { + return err + } + return e.Disconnect(reason) + } + return nil +} + +func (e *EventsListener) onChatMsg(p pk.Packet) error { + if e.ChatMsg != nil { + var msg chat.Message + var pos pk.Byte + var sender pk.UUID + + if err := p.Scan(&msg, &pos, &sender); err != nil { + return err + } + + return e.ChatMsg(msg, byte(pos), uuid.UUID(sender)) + } + return nil +} + +func (e *EventsListener) onUpdateHealth(p pk.Packet) error { + if e.ChatMsg != nil { + var health pk.Float + var food pk.VarInt + var foodSaturation pk.Float + + if err := p.Scan(&health, &food, &foodSaturation); err != nil { + return err + } + if e.HealthChange != nil { + if err := e.HealthChange(float32(health)); err != nil { + return err + } + } + if e.Death != nil && health <= 0 { + if err := e.Death(); err != nil { + return err + } + } + } + return nil +} diff --git a/bot/basic/info.go b/bot/basic/info.go new file mode 100644 index 0000000..4fd0a7d --- /dev/null +++ b/bot/basic/info.go @@ -0,0 +1,79 @@ +package basic + +import ( + "github.com/Tnze/go-mc/data/packetid" + pk "github.com/Tnze/go-mc/net/packet" + "unsafe" +) + +// WorldInfo content player info in server. +type WorldInfo struct { + DimensionCodec struct { + DimensionType interface{} `nbt:"minecraft:dimension_type"` + WorldgenBiome interface{} `nbt:"minecraft:worldgen/biome"` + } + Dimension interface{} + WorldNames []string // Identifiers for all worlds on the server. + WorldName string // Name of the world being spawned into. + HashedSeed int64 // First 8 bytes of the SHA-256 hash of the world's seed. Used client side for biome noise + MaxPlayers int32 // Was once used by the client to draw the player list, but now is ignored. + ViewDistance int32 // Render distance (2-32). + ReducedDebugInfo bool // If true, a Notchian client shows reduced information on the debug screen. For servers in development, this should almost always be false. + EnableRespawnScreen bool // Set to false when the doImmediateRespawn gamerule is true. + IsDebug bool // True if the world is a debug mode world; debug mode worlds cannot be modified and have predefined blocks. + IsFlat bool // True if the world is a superflat world; flat worlds have different void fog and a horizon at y=0 instead of y=63. +} + +type PlayerInfo struct { + EID int32 // The player's Entity ID (EID). + Hardcore bool // Is hardcore + Gamemode byte // Gamemode. 0: Survival, 1: Creative, 2: Adventure, 3: Spectator. + PrevGamemode int8 // Previous Gamemode +} + +// ServInfo contains information about the server implementation. +type ServInfo struct { + Brand string +} + +func (p *Player) handleJoinGamePacket(packet pk.Packet) error { + var WorldCount pk.VarInt + var WorldNames = []pk.Identifier{} + err := packet.Scan( + (*pk.Int)(&p.EID), + (*pk.Boolean)(&p.Hardcore), + (*pk.UnsignedByte)(&p.Gamemode), + (*pk.Byte)(&p.PrevGamemode), + &WorldCount, + pk.Ary{Len: &WorldCount, Ary: &WorldNames}, + &pk.NBT{V: new(interface{})}, + &pk.NBT{V: new(interface{})}, + (*pk.Identifier)(&p.WorldName), + (*pk.Long)(&p.HashedSeed), + (*pk.VarInt)(&p.MaxPlayers), + (*pk.VarInt)(&p.ViewDistance), + (*pk.Boolean)(&p.ReducedDebugInfo), + (*pk.Boolean)(&p.EnableRespawnScreen), + (*pk.Boolean)(&p.IsDebug), + (*pk.Boolean)(&p.IsFlat), + ) + if err != nil { + return err + } + + // This line should work "like" the following code but without copy things + // p.WorldNames = make([]string, len(WorldNames)) + // for i, v := range WorldNames { + // p.WorldNames[i] = string(v) + // } + p.WorldNames = *(*[]string)(unsafe.Pointer(&WorldNames)) + + return p.c.Conn.WritePacket( + //PluginMessage packet (serverbound) - sending minecraft brand. + pk.Marshal( + packetid.CustomPayloadServerbound, + pk.Identifier("minecraft:brand"), + pk.String(p.Settings.Brand), + ), + ) +} diff --git a/bot/basic/keepalive.go b/bot/basic/keepalive.go new file mode 100644 index 0000000..1b424bd --- /dev/null +++ b/bot/basic/keepalive.go @@ -0,0 +1,18 @@ +package basic + +import ( + "github.com/Tnze/go-mc/data/packetid" + pk "github.com/Tnze/go-mc/net/packet" +) + +func (p Player) handleKeepAlivePacket(packet pk.Packet) error { + var KeepAliveID pk.Long + if err := packet.Scan(&KeepAliveID); err != nil { + return err + } + // Response + return p.c.Conn.WritePacket(pk.Marshal( + packetid.KeepAliveServerbound, + KeepAliveID, + )) +} diff --git a/bot/settings.go b/bot/basic/settings.go similarity index 98% rename from bot/settings.go rename to bot/basic/settings.go index ae64edd..c14d261 100644 --- a/bot/settings.go +++ b/bot/basic/settings.go @@ -1,4 +1,4 @@ -package bot +package basic // Settings of client type Settings struct { diff --git a/bot/client.go b/bot/client.go index 40f6fa9..71bbd7e 100644 --- a/bot/client.go +++ b/bot/client.go @@ -1,56 +1,24 @@ package bot import ( - "sync" - "time" - - "github.com/Tnze/go-mc/bot/path" - "github.com/Tnze/go-mc/bot/phy" - "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/bot/world/entity" - "github.com/Tnze/go-mc/bot/world/entity/player" - "github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/net" - "github.com/Tnze/go-mc/net/packet" - pk "github.com/Tnze/go-mc/net/packet" + "github.com/google/uuid" ) // Client is used to access Minecraft server type Client struct { - conn *net.Conn - Auth + Conn *net.Conn + Auth Auth - player.Player - PlayInfo - ServInfo - abilities PlayerAbilities - settings Settings + Name string + UUID uuid.UUID - Wd world.World //the map data - Inputs path.Inputs - Physics phy.State - lastPosTx time.Time - justTeleported bool - - // Delegate allows you push a function to let HandleGame run. - // Do not send at the same goroutine! - Delegate chan func() error - Events eventBroker - - closing chan struct{} - inbound chan pk.Packet - wg sync.WaitGroup + Events Events + //TODO: LoginEvents Events } func (c *Client) Close() error { - close(c.closing) - err := c.disconnect() - c.wg.Wait() - return err -} - -func (c *Client) SendCloseWindow(windowID byte) error { - return c.conn.WritePacket(packet.Marshal(packetid.CloseWindowServerbound, pk.UnsignedByte(windowID))) + return c.Conn.Close() } // NewClient init and return a new Client. @@ -62,44 +30,11 @@ func (c *Client) SendCloseWindow(windowID byte) error { // and load your Name, UUID and AccessToken to client. func NewClient() *Client { return &Client{ - settings: DefaultSettings, - Auth: Auth{Name: "Steve"}, - Delegate: make(chan func() error), - Wd: world.World{ - Entities: make(map[int32]*entity.Entity, 8192), - Chunks: make(map[world.ChunkLoc]*world.Chunk, 2048), - }, - closing: make(chan struct{}), - inbound: make(chan pk.Packet, 5), + Auth: Auth{Name: "Steve"}, + Events: Events{handlers: make(map[int32]*handlerHeap)}, } } -//PlayInfo content player info in server. -type PlayInfo struct { - Gamemode int //游戏模式 - Hardcore bool //是否是极限模式 - Dimension int //维度 - Difficulty int //难度 - ViewDistance int //视距 - ReducedDebugInfo bool //减少调试信息 - WorldName string //当前世界的名字 - IsDebug bool //调试 - IsFlat bool //超平坦世界 - SpawnPosition Position //主世界出生点 -} - -// ServInfo contains information about the server implementation. -type ServInfo struct { - Brand string -} - -// PlayerAbilities defines what player can do. -type PlayerAbilities struct { - Flags int8 - FlyingSpeed float32 - FieldofViewModifier float32 -} - //Position is a 3D vector. type Position struct { X, Y, Z int diff --git a/bot/event.go b/bot/event.go index e79f5e5..465f421 100644 --- a/bot/event.go +++ b/bot/event.go @@ -1,89 +1,42 @@ package bot import ( - "github.com/Tnze/go-mc/bot/world/entity" - "github.com/Tnze/go-mc/bot/world/entity/player" - "github.com/Tnze/go-mc/chat" - "github.com/google/uuid" - pk "github.com/Tnze/go-mc/net/packet" - "github.com/Tnze/go-mc/net/ptypes" ) -type seenPacketFlags uint8 - -// Valid seenPacketFlags values. -const ( - seenJoinGame seenPacketFlags = 1 << iota - seenServerDifficulty - seenPlayerAbilities - seenPlayerInventory - seenUpdateLight - seenChunkData - seenPlayerPositionAndLook - seenSpawnPos - - // gameReadyMinPackets are the minimum set of packets that must be seen, for - // the GameReady callback to be invoked. - gameReadyMinPackets = seenJoinGame | seenChunkData | seenUpdateLight | - seenPlayerAbilities | seenPlayerInventory | seenServerDifficulty | - seenPlayerPositionAndLook | seenSpawnPos -) - -type eventBroker struct { - seenPackets seenPacketFlags - isReady bool - - GameStart func() error - ChatMsg func(msg chat.Message, pos byte, sender uuid.UUID) error - Disconnect func(reason chat.Message) error - HealthChange func() error - Die func() error - SoundPlay func(name string, category int, x, y, z float64, volume, pitch float32) error - PluginMessage func(channel string, data []byte) error - HeldItemChange func(slot int) error - OpenWindow func(pkt ptypes.OpenWindow) error - - // ExperienceChange will be called every time player's experience level updates. - // Parameters: - // bar - state of the experience bar from 0.0 to 1.0; - // level - current level; - // total - total amount of experience received from level 0. - ExperienceChange func(bar float32, level int32, total int32) error - - WindowsItem func(id byte, slots []entity.Slot) error - WindowsItemChange func(id byte, slotID int, slot entity.Slot) error - WindowConfirmation func(pkt ptypes.ConfirmTransaction) error - - // ServerDifficultyChange is called whenever the gamemode of the server changes. - // At time of writing (1.16.3), difficulty values of 0, 1, 2, and 3 correspond - // to peaceful, easy, normal, and hard respectively. - ServerDifficultyChange func(difficulty int) error - - // GameReady is called after the client has joined the server and successfully - // received player state. Additionally, the server has begun sending world - // state (such as lighting and chunk information). - // - // Use this callback as a signal as to when your bot should start 'doing' - // things. - GameReady func() error - - // PositionChange is called whenever the player position is updated. - PositionChange func(pos player.Pos) error - - // ReceivePacket will be called when new packets arrive. - // The default handler will run only if pass == false. - ReceivePacket func(p pk.Packet) (pass bool, err error) - - // PrePhysics will be called before a physics tick. - PrePhysics func() error +type Events struct { + handlers map[int32]*handlerHeap } -func (b *eventBroker) updateSeenPackets(f seenPacketFlags) error { - b.seenPackets |= f - if (^b.seenPackets)&gameReadyMinPackets == 0 && b.GameReady != nil && !b.isReady { - b.isReady = true - return b.GameReady() +func (e *Events) AddListener(listeners ...PacketHandler) { + for _, l := range listeners { + var s *handlerHeap + var ok bool + if s, ok = e.handlers[l.ID]; !ok { + s = &handlerHeap{l} + e.handlers[l.ID] = s + } + s.Push(l) } - return nil +} + +type PacketHandlerFunc func(p pk.Packet) error +type PacketHandler struct { + ID int32 + Priority int + F func(p pk.Packet) error +} + +// handlerHeap is PriorityQueue +type handlerHeap []PacketHandler + +func (h handlerHeap) Len() int { return len(h) } +func (h handlerHeap) Less(i, j int) bool { return h[i].Priority < h[j].Priority } +func (h handlerHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h *handlerHeap) Push(x interface{}) { *h = append(*h, x.(PacketHandler)) } +func (h *handlerHeap) Pop() interface{} { + old := *h + n := len(old) + *h = old[0 : n-1] + return old[n-1] } diff --git a/bot/ingame.go b/bot/ingame.go index c89620a..1f08f1e 100644 --- a/bot/ingame.go +++ b/bot/ingame.go @@ -1,784 +1,49 @@ package bot import ( - "bytes" "fmt" - "net/http" - "os" - "time" - - "github.com/google/uuid" - - "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/bot/world/entity" - "github.com/Tnze/go-mc/bot/world/entity/player" - "github.com/Tnze/go-mc/chat" - "github.com/Tnze/go-mc/data/packetid" - "github.com/Tnze/go-mc/data/soundid" pk "github.com/Tnze/go-mc/net/packet" - "github.com/Tnze/go-mc/net/ptypes" ) -func (c *Client) updateServerPos(pos player.Pos) (err error) { - prev := c.Player.Pos - c.Player.Pos = pos - - switch { - case !prev.LookEqual(pos) && !prev.PosEqual(pos): - err = sendPlayerPositionAndLookPacket(c) - case !prev.PosEqual(pos): - err = sendPlayerPositionPacket(c) - case !prev.LookEqual(pos): - err = sendPlayerLookPacket(c) - case prev.OnGround != pos.OnGround: - err = c.conn.WritePacket( - pk.Marshal( - packetid.Flying, - pk.Boolean(pos.OnGround), - ), - ) - } - if err != nil { - return err - } - - if c.justTeleported || time.Now().Add(-time.Second).After(c.lastPosTx) { - c.justTeleported = false - c.lastPosTx = time.Now() - err = sendPlayerPositionPacket(c) - if err != nil { - return err - } - } - - if c.Events.PositionChange != nil && !prev.Equal(pos) { - if err := c.Events.PositionChange(pos); err != nil { - return err - } - } - return nil -} - // HandleGame receive server packet and response them correctly. // Note that HandleGame will block if you don't receive from Events. func (c *Client) HandleGame() error { - c.wg.Add(1) - go func() { - defer c.wg.Done() - for { - select { - case <-c.closing: - return - - default: - var p pk.Packet - //Read packets - if err := c.conn.ReadPacket(&p); err != nil { - return - } - c.inbound <- p - } - } - }() - - cTick := time.NewTicker(time.Second / 10 / 2) - defer cTick.Stop() + var p pk.Packet for { - select { - case <-c.closing: - return http.ErrServerClosed - case <-cTick.C: - if c.Events.PrePhysics != nil { - if err := c.Events.PrePhysics(); err != nil { - return err - } - } - if err := c.Physics.Tick(c.Inputs, &c.Wd); err != nil { - c.disconnect() - return err - } - c.updateServerPos(c.Physics.Position()) + //Read packets + if err := c.Conn.ReadPacket(&p); err != nil { + return err + } - case task := <-c.Delegate: - if err := task(); err != nil { - c.disconnect() - return err - } - case p := <-c.inbound: - //handle packets - disconnect, err := c.handlePacket(p) - if err != nil { - c.disconnect() - return fmt.Errorf("handle packet 0x%X error: %w", p.ID, err) - } - if disconnect { - return nil - } + //handle packets + err := c.handlePacket(p) + if err != nil { + return err } } } -func (c *Client) handlePacket(p pk.Packet) (disconnect bool, err error) { - if c.Events.ReceivePacket != nil { - pass, err := c.Events.ReceivePacket(p) - if err != nil { - return false, err +type PacketHandlerError struct { + ID int32 + Err error +} + +func (d PacketHandlerError) Error() string { + return fmt.Sprintf("handle packet 0x%X error: %v", d.ID, d.Err) +} + +func (d PacketHandlerError) Unwrap() error { + return d.Err +} + +func (c *Client) handlePacket(p pk.Packet) (err error) { + if listeners := c.Events.handlers[p.ID]; listeners != nil { + for _, handler := range *listeners { + err = handler.F(p) + if err != nil { + return PacketHandlerError{ID: p.ID, Err: err} + } } - if pass { - return false, nil - } - } - - switch p.ID { - case packetid.Login: - err = handleJoinGamePacket(c, p) - case packetid.CustomPayloadClientbound: - err = handlePluginPacket(c, p) - case packetid.Difficulty: - err = handleServerDifficultyPacket(c, p) - case packetid.SpawnPosition: - err = handleSpawnPositionPacket(c, p) - if err2 := c.Events.updateSeenPackets(seenSpawnPos); err == nil { - err = err2 - } - case packetid.AbilitiesClientbound: - err = handlePlayerAbilitiesPacket(c, p) - case packetid.UpdateHealth: - err = handleUpdateHealthPacket(c, p) - case packetid.ChatClientbound: - err = handleChatMessagePacket(c, p) - - case packetid.HeldItemSlotClientbound: - err = handleHeldItemPacket(c, p) - case packetid.WindowItems: - err = handleWindowItemsPacket(c, p) - case packetid.OpenWindow: - err = handleOpenWindowPacket(c, p) - case packetid.TransactionClientbound: - err = handleWindowConfirmationPacket(c, p) - - case packetid.DeclareRecipes: - // handleDeclareRecipesPacket(g, reader) - case packetid.KeepAliveClientbound: - err = handleKeepAlivePacket(c, p) - - case packetid.SpawnEntity: - err = handleSpawnEntityPacket(c, p) - case packetid.NamedEntitySpawn: - err = handleSpawnPlayerPacket(c, p) - case packetid.SpawnEntityLiving: - err = handleSpawnLivingEntityPacket(c, p) - case packetid.Animation: - err = handleEntityAnimationPacket(c, p) - case packetid.EntityStatus: - err = handleEntityStatusPacket(c, p) - case packetid.EntityDestroy: - err = handleDestroyEntitiesPacket(c, p) - case packetid.RelEntityMove: - err = handleEntityPositionPacket(c, p) - case packetid.EntityMoveLook: - err = handleEntityPositionLookPacket(c, p) - case packetid.EntityLook: - err = handleEntityLookPacket(c, p) - case packetid.Entity: - err = handleEntityMovePacket(c, p) - - case packetid.UpdateLight: - err = c.Events.updateSeenPackets(seenUpdateLight) - case packetid.MapChunk: - err = handleChunkDataPacket(c, p) - case packetid.BlockChange: - err = handleBlockChangePacket(c, p) - case packetid.MultiBlockChange: - err = handleMultiBlockChangePacket(c, p) - case packetid.UnloadChunk: - err = handleUnloadChunkPacket(c, p) - case packetid.TileEntityData: - err = handleTileEntityDataPacket(c, p) - - case packetid.PositionClientbound: - err = handlePlayerPositionAndLookPacket(c, p) - sendPlayerPositionAndLookPacket(c) // to confirm the position - if err2 := c.Events.updateSeenPackets(seenPlayerPositionAndLook); err == nil { - err = err2 - } - - case packetid.KickDisconnect: - err = handleDisconnectPacket(c, p) - disconnect = true - case packetid.SetSlot: - err = handleSetSlotPacket(c, p) - case packetid.SoundEffect: - err = handleSoundEffect(c, p) - case packetid.NamedSoundEffect: - err = handleNamedSoundEffect(c, p) - case packetid.Experience: - err = handleSetExperience(c, p) } return } - -func handleSpawnEntityPacket(c *Client, p pk.Packet) error { - var se ptypes.SpawnEntity - if err := se.Decode(p); err != nil { - return err - } - return c.Wd.OnSpawnEntity(se) -} - -func handleSpawnLivingEntityPacket(c *Client, p pk.Packet) error { - var se ptypes.SpawnLivingEntity - if err := se.Decode(p); err != nil { - return err - } - return c.Wd.OnSpawnLivingEntity(se) -} - -func handleSpawnPlayerPacket(c *Client, p pk.Packet) error { - var se ptypes.SpawnPlayer - if err := se.Decode(p); err != nil { - return err - } - return c.Wd.OnSpawnPlayer(se) -} - -func handleEntityPositionPacket(c *Client, p pk.Packet) error { - var pu ptypes.EntityPosition - if err := pu.Decode(p); err != nil { - return err - } - return c.Wd.OnEntityPosUpdate(pu) -} - -func handleEntityPositionLookPacket(c *Client, p pk.Packet) error { - var epr ptypes.EntityPositionLook - if err := epr.Decode(p); err != nil { - return err - } - return c.Wd.OnEntityPosLookUpdate(epr) -} - -func handleEntityLookPacket(c *Client, p pk.Packet) error { - var er ptypes.EntityRotation - if err := er.Decode(p); err != nil { - return err - } - return c.Wd.OnEntityLookUpdate(er) -} - -func handleEntityMovePacket(c *Client, p pk.Packet) error { - var id pk.VarInt - if err := p.Scan(&id); err != nil { - return err - } - return nil -} - -func handleEntityAnimationPacket(c *Client, p pk.Packet) error { - var se ptypes.EntityAnimationClientbound - if err := se.Decode(p); err != nil { - return err - } - return nil -} - -func handleEntityStatusPacket(c *Client, p pk.Packet) error { - var ( - id pk.Int - status pk.Byte - ) - if err := p.Scan(&id, &status); err != nil { - return err - } - return nil -} - -func handleDestroyEntitiesPacket(c *Client, p pk.Packet) error { - var count pk.VarInt - var data = pk.Ary{ - Len: &count, - Ary: []pk.VarInt{}, - } - if err := p.Scan(&count, &data); err != nil { - return err - } - - return c.Wd.OnEntityDestroy(data.Ary.([]pk.VarInt)) -} - -func handleSoundEffect(c *Client, p pk.Packet) error { - var s ptypes.SoundEffect - if err := s.Decode(p); err != nil { - return err - } - - if c.Events.SoundPlay != nil { - if soundName, ok := soundid.GetSoundNameByID(soundid.SoundID(s.Sound)); ok { - return c.Events.SoundPlay( - soundName, int(s.Category), - float64(s.X)/8, float64(s.Y)/8, float64(s.Z)/8, - float32(s.Volume), float32(s.Pitch)) - } - - fmt.Fprintf(os.Stderr, "WARN: Unknown sound name (%v). is data.SoundNames out of date?\n", s.Sound) - } - - return nil -} - -func handleNamedSoundEffect(c *Client, p pk.Packet) error { - var s ptypes.NamedSoundEffect - if err := s.Decode(p); err != nil { - return err - } - - if c.Events.SoundPlay != nil { - return c.Events.SoundPlay( - string(s.Sound), int(s.Category), - float64(s.X)/8, float64(s.Y)/8, float64(s.Z)/8, - float32(s.Volume), float32(s.Pitch)) - } - - return nil -} - -func handleDisconnectPacket(c *Client, p pk.Packet) error { - var reason chat.Message - - err := p.Scan(&reason) - if err != nil { - return err - } - - if c.Events.Disconnect != nil { - return c.Events.Disconnect(reason) - } - return nil -} - -func handleSetSlotPacket(c *Client, p pk.Packet) error { - if c.Events.WindowsItemChange == nil { - return nil - } - var pkt ptypes.SetSlot - if err := pkt.Decode(p); err != nil { - return err - } - - return c.Events.WindowsItemChange(byte(pkt.WindowID), int(pkt.Slot), pkt.SlotData) -} - -func handleMultiBlockChangePacket(c *Client, p pk.Packet) error { - if !c.settings.ReceiveMap { - return nil - } - - var ( - loc pk.Long - dontTrustEdges pk.Boolean - sz pk.VarInt - packedBlocks = pk.Ary{ - Len: &sz, - Ary: []pk.VarLong{}, - } - ) - err := p.Scan(&loc, &dontTrustEdges, &sz, &packedBlocks) - if err != nil { - return err - } - - x := int((loc >> 42) & ((1 << 22) - 1)) - y := int((loc >> 20) & ((1 << 22) - 1)) - z := int(loc & ((1 << 20) - 1)) - - // Apply transform into negative (these numbers are signed) - if x >= 1<<21 { - x -= 1 << 22 - } - if z >= 1<<21 { - z -= 1 << 22 - } - - c.Wd.MultiBlockUpdate(world.ChunkLoc{X: x, Z: z}, y, packedBlocks.Ary.([]pk.VarLong)) - return nil -} - -func handleBlockChangePacket(c *Client, p pk.Packet) error { - if !c.settings.ReceiveMap { - return nil - } - var ( - pos pk.Position - bID pk.VarInt - ) - - if err := p.Scan(&pos, &bID); err != nil { - return err - } - - c.Wd.UnaryBlockUpdate(pos, world.BlockStatus(bID)) - return nil -} - -func handleChatMessagePacket(c *Client, p pk.Packet) (err error) { - var msg ptypes.ChatMessageClientbound - if err := msg.Decode(p); err != nil { - return err - } - - if c.Events.ChatMsg != nil { - return c.Events.ChatMsg(msg.S, byte(msg.Pos), uuid.UUID(msg.Sender)) - } - return nil -} - -func handleUpdateHealthPacket(c *Client, p pk.Packet) error { - var pkt ptypes.UpdateHealth - if err := pkt.Decode(p); err != nil { - return err - } - - c.Health = float32(pkt.Health) - c.Food = int32(pkt.Food) - c.FoodSaturation = float32(pkt.FoodSaturation) - - if c.Events.HealthChange != nil { - if err := c.Events.HealthChange(); err != nil { - return err - } - } - if c.Health <= 0 { //player is dead - c.Physics.Run = false - sendPlayerPositionAndLookPacket(c) - if c.Events.Die != nil { - if err := c.Events.Die(); err != nil { - return err - } - } - } - return nil -} - -func handleJoinGamePacket(c *Client, p pk.Packet) error { - var pkt ptypes.JoinGame - if err := pkt.Decode(p); err != nil { - return err - } - - c.Player.ID = int32(pkt.PlayerEntity) - c.Gamemode = int(pkt.Gamemode & 0x7) - c.Hardcore = pkt.Gamemode&0x8 != 0 - c.Dimension = int(pkt.Dimension) - c.WorldName = string(pkt.WorldName) - c.ViewDistance = int(pkt.ViewDistance) - c.ReducedDebugInfo = bool(pkt.RDI) - c.IsDebug = bool(pkt.IsDebug) - c.IsFlat = bool(pkt.IsFlat) - - if c.Events.GameStart != nil { - if err := c.Events.GameStart(); err != nil { - return err - } - } - - c.conn.WritePacket( - //PluginMessage packet (serverbound) - sending minecraft brand. - pk.Marshal( - packetid.CustomPayloadServerbound, - pk.Identifier("minecraft:brand"), - pk.String(c.settings.Brand), - ), - ) - if err := c.Events.updateSeenPackets(seenJoinGame); err != nil { - return err - } - return nil -} - -func handlePluginPacket(c *Client, p pk.Packet) error { - var msg ptypes.PluginMessage - if err := msg.Decode(p); err != nil { - return err - } - - switch msg.Channel { - case "minecraft:brand": - var brandRaw pk.String - if _, err := brandRaw.ReadFrom(bytes.NewReader(msg.Data)); err != nil { - return err - } - c.ServInfo.Brand = string(brandRaw) - } - - if c.Events.PluginMessage != nil { - return c.Events.PluginMessage(string(msg.Channel), msg.Data) - } - return nil -} - -func handleServerDifficultyPacket(c *Client, p pk.Packet) error { - var difficulty pk.Byte - if err := p.Scan(&difficulty); err != nil { - return err - } - c.Difficulty = int(difficulty) - - if c.Events.ServerDifficultyChange != nil { - if err := c.Events.ServerDifficultyChange(c.Difficulty); err != nil { - return err - } - } - return c.Events.updateSeenPackets(seenServerDifficulty) -} - -func handleSpawnPositionPacket(c *Client, p pk.Packet) error { - var pos pk.Position - err := p.Scan(&pos) - if err != nil { - return err - } - c.SpawnPosition.X, c.SpawnPosition.Y, c.SpawnPosition.Z = - pos.X, pos.Y, pos.Z - return nil -} - -func handlePlayerAbilitiesPacket(c *Client, p pk.Packet) error { - var ( - flags pk.Byte - flySpeed pk.Float - viewMod pk.Float - ) - err := p.Scan(&flags, &flySpeed, &viewMod) - if err != nil { - return err - } - c.abilities.Flags = int8(flags) - c.abilities.FlyingSpeed = float32(flySpeed) - c.abilities.FieldofViewModifier = float32(viewMod) - - c.conn.WritePacket( - //ClientSettings packet (serverbound) - pk.Marshal( - packetid.Settings, - pk.String(c.settings.Locale), - pk.Byte(c.settings.ViewDistance), - pk.VarInt(c.settings.ChatMode), - pk.Boolean(c.settings.ChatColors), - pk.UnsignedByte(c.settings.DisplayedSkinParts), - pk.VarInt(c.settings.MainHand), - ), - ) - return c.Events.updateSeenPackets(seenPlayerAbilities) -} - -func handleHeldItemPacket(c *Client, p pk.Packet) error { - var hi pk.Byte - if err := p.Scan(&hi); err != nil { - return err - } - c.HeldItem = int(hi) - - if c.Events.HeldItemChange != nil { - return c.Events.HeldItemChange(c.HeldItem) - } - return nil -} - -func handleUnloadChunkPacket(c *Client, p pk.Packet) error { - if !c.settings.ReceiveMap { - return nil - } - - var x, z pk.Int - if err := p.Scan(&x, &z); err != nil { - return err - } - c.Wd.UnloadChunk(world.ChunkLoc{X: int(x) >> 4, Z: int(z) >> 4}) - return nil -} - -func handleChunkDataPacket(c *Client, p pk.Packet) error { - if err := c.Events.updateSeenPackets(seenChunkData); err != nil { - return err - } - if !c.settings.ReceiveMap { - return nil - } - - var pkt ptypes.ChunkData - if _, err := pkt.ReadFrom(bytes.NewReader(p.Data)); err != nil { - return err - } - - chunk, err := world.DecodeChunkColumn(int32(pkt.PrimaryBitMask), pkt.Data) - if err != nil { - return fmt.Errorf("decode chunk column: %w", err) - } - chunk.TileEntities = make(map[world.TilePosition]entity.BlockEntity, 64) - for _, e := range pkt.BlockEntities { - chunk.TileEntities[world.ToTilePos(e.X, e.Y, e.Z)] = e - } - - c.Wd.LoadChunk(int(pkt.X), int(pkt.Z), chunk) - return nil -} - -func handleTileEntityDataPacket(c *Client, p pk.Packet) error { - var pkt ptypes.TileEntityData - if _, err := pkt.ReadFrom(bytes.NewReader(p.Data)); err != nil { - return err - } - return c.Wd.TileEntityUpdate(pkt) -} - -func handlePlayerPositionAndLookPacket(c *Client, p pk.Packet) error { - var pkt ptypes.PositionAndLookClientbound - if err := pkt.Decode(p); err != nil { - return err - } - - pp := c.Player.Pos - if pkt.RelativeX() { - pp.X += float64(pkt.X) - } else { - pp.X = float64(pkt.X) - } - if pkt.RelativeY() { - pp.Y += float64(pkt.Y) - } else { - pp.Y = float64(pkt.Y) - } - if pkt.RelativeZ() { - pp.Z += float64(pkt.Z) - } else { - pp.Z = float64(pkt.Z) - } - if pkt.RelativeYaw() { - pp.Yaw += float32(pkt.Yaw) - } else { - pp.Yaw = float32(pkt.Yaw) - } - if pkt.RelativePitch() { - pp.Pitch += float32(pkt.Pitch) - } else { - pp.Pitch = float32(pkt.Pitch) - } - if err := c.Physics.ServerPositionUpdate(pp, &c.Wd); err != nil { - return err - } - c.Player.Pos = pp - c.justTeleported = true - - if c.Events.PositionChange != nil { - if err := c.Events.PositionChange(pp); err != nil { - return err - } - } - - //Confirm - return c.conn.WritePacket(pk.Marshal( - packetid.TeleportConfirm, - pk.VarInt(pkt.TeleportID), - )) -} - -func handleKeepAlivePacket(c *Client, p pk.Packet) error { - var KeepAliveID pk.Long - if err := p.Scan(&KeepAliveID); err != nil { - return err - } - //Response - return c.conn.WritePacket(pk.Marshal( - packetid.KeepAliveServerbound, - KeepAliveID, - )) -} - -func handleWindowItemsPacket(c *Client, p pk.Packet) error { - var pkt ptypes.WindowItems - if _, err := pkt.ReadFrom(bytes.NewReader(p.Data)); err != nil { - return err - } - - if pkt.WindowID == 0 { // Window ID 0 is the players' inventory. - if err := c.Events.updateSeenPackets(seenPlayerInventory); err != nil { - return err - } - } - if c.Events.WindowsItem != nil { - return c.Events.WindowsItem(byte(pkt.WindowID), pkt.Slots) - } - return nil -} - -func handleOpenWindowPacket(c *Client, p pk.Packet) error { - var pkt ptypes.OpenWindow - if err := pkt.Decode(p); err != nil { - return err - } - - if c.Events.OpenWindow != nil { - return c.Events.OpenWindow(pkt) - } - return nil -} - -func handleWindowConfirmationPacket(c *Client, p pk.Packet) error { - var pkt ptypes.ConfirmTransaction - if err := pkt.Decode(p); err != nil { - return err - } - - if c.Events.WindowConfirmation != nil { - return c.Events.WindowConfirmation(pkt) - } - return nil -} - -func handleSetExperience(c *Client, p pk.Packet) (err error) { - var ( - bar pk.Float - level pk.VarInt - total pk.VarInt - ) - - if err := p.Scan(&bar, &level, &total); err != nil { - return err - } - - c.Level = int32(level) - - if c.Events.ExperienceChange != nil { - return c.Events.ExperienceChange(float32(bar), int32(level), int32(total)) - } - - return nil -} - -func sendPlayerPositionAndLookPacket(c *Client) error { - return c.conn.WritePacket(ptypes.PositionAndLookServerbound{ - X: pk.Double(c.Pos.X), - Y: pk.Double(c.Pos.Y), - Z: pk.Double(c.Pos.Z), - Yaw: pk.Float(c.Pos.Yaw), - Pitch: pk.Float(c.Pos.Pitch), - OnGround: pk.Boolean(c.Pos.OnGround), - }.Encode()) -} - -func sendPlayerPositionPacket(c *Client) error { - return c.conn.WritePacket(ptypes.Position{ - X: pk.Double(c.Pos.X), - Y: pk.Double(c.Pos.Y), - Z: pk.Double(c.Pos.Z), - OnGround: pk.Boolean(c.Pos.OnGround), - }.Encode()) -} - -func sendPlayerLookPacket(c *Client) error { - return c.conn.WritePacket(ptypes.Look{ - Yaw: pk.Float(c.Pos.Yaw), - Pitch: pk.Float(c.Pos.Pitch), - OnGround: pk.Boolean(c.Pos.OnGround), - }.Encode()) -} diff --git a/bot/login.go b/bot/login.go index c6d6a18..0b0cc0e 100644 --- a/bot/login.go +++ b/bot/login.go @@ -25,34 +25,35 @@ type Auth struct { AsTk string } -// 加密请求 -func handleEncryptionRequest(c *Client, pack pk.Packet) error { - //创建AES对称加密密钥 +func handleEncryptionRequest(c *Client, p pk.Packet) error { + // 创建AES对称加密密钥 key, encoStream, decoStream := newSymmetricEncryption() - //解析EncryptionRequest包 + // Read EncryptionRequest var er encryptionRequest - if err := pack.Scan(&er); err != nil { + if err := p.Scan(&er); err != nil { return err } - err := loginAuth(c.AsTk, c.Name, c.Auth.UUID, key, er) //向Mojang验证 + + err := loginAuth(c.Auth, key, er) //向Mojang验证 if err != nil { return fmt.Errorf("login fail: %v", err) } // 响应加密请求 - var p pk.Packet // Encryption Key Response + // Write Encryption Key Response p, err = genEncryptionKeyResponse(key, er.PublicKey, er.VerifyToken) if err != nil { return fmt.Errorf("gen encryption key response fail: %v", err) } - err = c.conn.WritePacket(p) + + err = c.Conn.WritePacket(p) if err != nil { return err } // 设置连接加密 - c.conn.SetCipher(encoStream, decoStream) + c.Conn.SetCipher(encoStream, decoStream) return nil } @@ -122,16 +123,16 @@ type request struct { ServerID string `json:"serverId"` } -func loginAuth(AsTk, name, UUID string, shareSecret []byte, er encryptionRequest) error { +func loginAuth(auth Auth, shareSecret []byte, er encryptionRequest) error { digest := authDigest(er.ServerID, shareSecret, er.PublicKey) client := http.Client{} requestPacket, err := json.Marshal( request{ - AccessToken: AsTk, + AccessToken: auth.AsTk, SelectedProfile: profile{ - ID: UUID, - Name: name, + ID: auth.UUID, + Name: auth.Name, }, ServerID: digest, }, @@ -155,7 +156,7 @@ func loginAuth(AsTk, name, UUID string, shareSecret []byte, er encryptionRequest defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) - if resp.Status != "204 No Content" { + if resp.StatusCode != http.StatusNoContent { return fmt.Errorf("auth fail: %s", string(body)) } return nil diff --git a/bot/mcbot.go b/bot/mcbot.go index 8b15972..cce5132 100644 --- a/bot/mcbot.go +++ b/bot/mcbot.go @@ -6,10 +6,10 @@ package bot import ( "context" - "fmt" "net" "strconv" + "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data/packetid" mcnet "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" @@ -22,9 +22,15 @@ const DefaultPort = 25565 // JoinServer connect a Minecraft server for playing the game. // Using roughly the same way to parse address as minecraft. func (c *Client) JoinServer(addr string) (err error) { - return c.JoinServerWithDialer(&net.Dialer{}, addr) + return c.join(&net.Dialer{}, addr) } +// JoinServerWithDialer is similar to JoinServer but using a Dialer. +func (c *Client) JoinServerWithDialer(d *net.Dialer, addr string) (err error) { + return c.join(d, addr) +} + +// parseAddress will lookup SRV records for the address func parseAddress(r *net.Resolver, addr string) (string, error) { const missingPort = "missing port in address" var port uint16 @@ -49,113 +55,110 @@ func parseAddress(r *net.Resolver, addr string) (string, error) { return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)), nil } -// JoinServerWithDialer is similar to JoinServer but using a Dialer. -func (c *Client) JoinServerWithDialer(d *net.Dialer, addr string) (err error) { - addr, err = parseAddress(d.Resolver, addr) +func (c *Client) join(d *net.Dialer, addr string) error { + const Handshake = 0x00 + addrSrv, err := parseAddress(d.Resolver, addr) if err != nil { - return fmt.Errorf("parse address error: %w", err) + return LoginErr{"resolved address", err} } - return c.join(d, addr) -} -func (c *Client) join(d *net.Dialer, addr string) (err error) { - conn, err := d.Dial("tcp", addr) + // Split Host and Port + host, portStr, err := net.SplitHostPort(addrSrv) if err != nil { - err = fmt.Errorf("bot: connect server fail: %v", err) - return err - } - //Set Conn - c.conn = mcnet.WrapConn(conn) - - //Get Host and Port - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - err = fmt.Errorf("bot: connect server fail: %v", err) - return err + return LoginErr{"split address", err} } port, err := strconv.Atoi(portStr) if err != nil { - err = fmt.Errorf("bot: connect server fail: %v", err) - return err + return LoginErr{"parse port", err} } - //Handshake - err = c.conn.WritePacket( - //Handshake Packet - pk.Marshal( - 0x00, //Handshake packet ID - pk.VarInt(ProtocolVersion), //Protocol version - pk.String(host), //Server's address - pk.UnsignedShort(port), - pk.Byte(2), - )) + // Dial connection + c.Conn, err = mcnet.DialMC(addrSrv) if err != nil { - err = fmt.Errorf("bot: send handshake packect fail: %v", err) - return + return LoginErr{"connect server", err} } - //Login - err = c.conn.WritePacket( - //LoginStart Packet - pk.Marshal(0, pk.String(c.Name))) + // Handshake + err = c.Conn.WritePacket(pk.Marshal( + Handshake, + pk.VarInt(ProtocolVersion), // Protocol version + pk.String(host), // Host + pk.UnsignedShort(port), // Port + pk.Byte(2), + )) if err != nil { - err = fmt.Errorf("bot: send login start packect fail: %v", err) - return + return LoginErr{"handshake", err} + } + + // Login Start + err = c.Conn.WritePacket(pk.Marshal( + packetid.LoginStart, + pk.String(c.Auth.Name), + )) + if err != nil { + return LoginErr{"login start", err} } for { - //Recive Packet - var pack pk.Packet - if err = c.conn.ReadPacket(&pack); err != nil { - err = fmt.Errorf("bot: recv packet for Login fail: %v", err) - return + //Receive Packet + var p pk.Packet + if err = c.Conn.ReadPacket(&p); err != nil { + return LoginErr{"receive packet", err} } //Handle Packet - switch pack.ID { - case 0x00: //Disconnect - var reason pk.String - err = pack.Scan(&reason) + switch p.ID { + case packetid.Disconnect: //Disconnect + var reason chat.Message + err = p.Scan(&reason) if err != nil { - err = fmt.Errorf("bot: read Disconnect message fail: %v", err) - } else { - err = fmt.Errorf("bot: connect disconnected by server: %s", reason) + return LoginErr{"disconnect", err} } - return - case 0x01: //Encryption Request - if err := handleEncryptionRequest(c, pack); err != nil { - return fmt.Errorf("bot: encryption fail: %v", err) + return LoginErr{"disconnect", DisconnectErr(reason)} + + case packetid.EncryptionBeginClientbound: //Encryption Request + if err := handleEncryptionRequest(c, p); err != nil { + return LoginErr{"encryption", err} + } + + case packetid.Success: //Login Success + err := p.Scan( + (*pk.UUID)(&c.UUID), + (*pk.String)(&c.Name), + ) + if err != nil { + return LoginErr{"login success", err} } - case 0x02: //Login Success - // uuid, l := pk.UnpackString(pack.Data) - // name, _ := unpackString(pack.Data[l:]) return nil - case 0x03: //Set Compression + + case packetid.Compress: //Set Compression var threshold pk.VarInt - if err := pack.Scan(&threshold); err != nil { - return fmt.Errorf("bot: set compression fail: %v", err) - } - c.conn.SetThreshold(int(threshold)) - case 0x04: //Login Plugin Request - if err := handlePluginPacket(c, pack); err != nil { - return fmt.Errorf("bot: handle plugin packet fail: %v", err) + if err := p.Scan(&threshold); err != nil { + return LoginErr{"compression", err} } + c.Conn.SetThreshold(int(threshold)) + + case packetid.LoginPluginRequest: //Login Plugin Request + // TODO: Handle login plugin request } } } -// Conn return the MCConn of the Client. -// Only used when you want to handle the packets by yourself -func (c *Client) Conn() *mcnet.Conn { - return c.conn +type LoginErr struct { + Stage string + Err error } -// SendMessage sends a chat message. -func (c *Client) SendMessage(msg string) error { - return c.conn.WritePacket( - pk.Marshal( - packetid.ChatServerbound, - pk.String(msg), - ), - ) +func (l LoginErr) Error() string { + return "bot: " + l.Stage + " error: " + l.Err.Error() +} + +func (l LoginErr) Unwrap() error { + return l.Err +} + +type DisconnectErr chat.Message + +func (d DisconnectErr) Error() string { + return "disconnect because: " + chat.Message(d).String() } diff --git a/bot/motion.go b/bot/motion.go deleted file mode 100644 index c6b90b7..0000000 --- a/bot/motion.go +++ /dev/null @@ -1,190 +0,0 @@ -package bot - -import ( - "errors" - "strconv" - - "github.com/Tnze/go-mc/data/packetid" - pk "github.com/Tnze/go-mc/net/packet" - "github.com/Tnze/go-mc/net/ptypes" -) - -// SwingArm swing player's arm. -// hand could be one of 0: main hand, 1: off hand. -// It's just animation. -func (c *Client) SwingArm(hand int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.ArmAnimation, - pk.VarInt(hand), - )) -} - -// Respawn the player when it was dead. -func (c *Client) Respawn() error { - return c.conn.WritePacket(pk.Marshal( - packetid.ClientCommand, - pk.VarInt(0), - )) -} - -// UseItem use the item player handing. -// hand could be one of 0: main hand, 1: off hand -func (c *Client) UseItem(hand int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.UseItem, - pk.VarInt(hand), - )) -} - -// UseEntity used by player to right-clicks another entity. -// hand could be one of 0: main hand, 1: off hand. -// A Notchian server only accepts this packet if -// the entity being attacked/used is visible without obstruction -// and within a 4-unit radius of the player's position. -func (c *Client) UseEntity(entityID int32, hand int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.UseEntity, - pk.VarInt(entityID), - pk.VarInt(0), - pk.VarInt(hand), - )) -} - -// AttackEntity used by player to left-clicks another entity. -// The attack version of UseEntity. Has the same limit. -func (c *Client) AttackEntity(entityID int32, hand int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.UseEntity, - pk.VarInt(entityID), - pk.VarInt(1), - )) -} - -// UseEntityAt is a variety of UseEntity with target location -func (c *Client) UseEntityAt(entityID int32, x, y, z float32, hand int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.UseEntity, - pk.VarInt(entityID), - pk.VarInt(2), - pk.Float(x), pk.Float(y), pk.Float(z), - pk.VarInt(hand), - )) -} - -// Chat send chat as chat message or command at textbox. -func (c *Client) Chat(msg string) error { - if len(msg) > 256 { - return errors.New("message too long") - } - - return c.conn.WritePacket(pk.Marshal( - packetid.ChatServerbound, - pk.String(msg), - )) -} - -// PluginMessage is used by mods and plugins to send their data. -func (c *Client) PluginMessage(channel string, msg []byte) error { - return c.conn.WritePacket((&ptypes.PluginMessage{ - Channel: pk.Identifier(channel), - Data: ptypes.PluginData(msg), - }).Encode()) -} - -// UseBlock is used to place or use a block. -// hand is the hand from which the block is placed; 0: main hand, 1: off hand. -// face is the face on which the block is placed. -// -// Cursor position is the position of the crosshair on the block: -// cursorX, from 0 to 1 increasing from west to east; -// cursorY, from 0 to 1 increasing from bottom to top; -// cursorZ, from 0 to 1 increasing from north to south. -// -// insideBlock is true when the player's head is inside of a block's collision. -func (c *Client) UseBlock(hand, locX, locY, locZ, face int, cursorX, cursorY, cursorZ float32, insideBlock bool) error { - return c.conn.WritePacket(pk.Marshal( - packetid.UseItem, - pk.VarInt(hand), - pk.Position{X: locX, Y: locY, Z: locZ}, - pk.VarInt(face), - pk.Float(cursorX), pk.Float(cursorY), pk.Float(cursorZ), - pk.Boolean(insideBlock), - )) -} - -// SelectItem used to change the slot selection in hotbar. -// slot should from 0 to 8 -func (c *Client) SelectItem(slot int) error { - if slot < 0 || slot > 8 { - return errors.New("invalid slot: " + strconv.Itoa(slot)) - } - - return c.conn.WritePacket(pk.Marshal( - packetid.HeldItemSlotServerbound, - pk.Short(slot), - )) -} - -// PickItem used to swap out an empty space on the hotbar with the item in the given inventory slot. -// The Notchain client uses this for pick block functionality (middle click) to retrieve items from the inventory. -// -// The server will first search the player's hotbar for an empty slot, -// starting from the current slot and looping around to the slot before it. -// If there are no empty slots, it will start a second search from the -// current slot and find the first slot that does not contain an enchanted item. -// If there still are no slots that meet that criteria, then the server will -// use the currently selected slot. After finding the appropriate slot, -// the server swaps the items and then change player's selected slot (cause the HeldItemChange event). -func (c *Client) PickItem(slot int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.PickItem, - pk.VarInt(slot), - )) -} - -func (c *Client) playerAction(status, locX, locY, locZ, face int) error { - return c.conn.WritePacket(pk.Marshal( - packetid.BlockDig, - pk.VarInt(status), - pk.Position{X: locX, Y: locY, Z: locZ}, - pk.Byte(face), - )) -} - -// Dig used to start, end or cancel a digging -// status is 0 for start digging, 1 for cancel and 2 if client think it done. -// To digging a block without cancel, use status 0 and 2 once each. -func (c *Client) Dig(status, locX, locY, locZ, face int) error { - return c.playerAction(status, locX, locY, locZ, face) -} - -// DropItemStack drop the entire selected stack -func (c *Client) DropItemStack() error { - return c.playerAction(3, 0, 0, 0, 0) -} - -// DropItem drop one item in selected stack -func (c *Client) DropItem() error { - return c.playerAction(4, 0, 0, 0, 0) -} - -// UseItemEnd used to finish UseItem, like eating food, pulling back bows. -func (c *Client) UseItemEnd() error { - return c.playerAction(5, 0, 0, 0, 0) -} - -// SwapItem used to swap the items in hands. -func (c *Client) SwapItem() error { - return c.playerAction(6, 0, 0, 0, 0) -} - -// Disconnect disconnect the server. -// Server will close the connection. -func (c *Client) disconnect() error { - return c.conn.Close() -} - -// SendPacket send the packet to server. -func (c *Client) SendPacket(packet pk.Packet) error { - return c.conn.WritePacket(packet) -} diff --git a/bot/path/blocks.go b/bot/path/blocks.go deleted file mode 100644 index 5339b2e..0000000 --- a/bot/path/blocks.go +++ /dev/null @@ -1,179 +0,0 @@ -package path - -import ( - "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/data/block" -) - -var ( - safeStepBlocks = make(map[world.BlockStatus]struct{}, 1024) - stepBlocks = []block.Block{ - block.Stone, - block.Granite, - block.PolishedGranite, - block.Diorite, - block.PolishedDiorite, - block.Andesite, - block.PolishedAndesite, - block.GrassBlock, - block.GrassPath, - block.Dirt, - block.CoarseDirt, - block.Cobblestone, - block.OakPlanks, - block.SprucePlanks, - block.BirchPlanks, - block.JunglePlanks, - block.AcaciaPlanks, - block.DarkOakPlanks, - block.Bedrock, - block.GoldOre, - block.IronOre, - block.CoalOre, - block.Glass, - block.LapisOre, - block.Sandstone, - block.RedstoneOre, - block.OakStairs, - block.AcaciaStairs, - block.DarkOakStairs, - block.RedSandstoneStairs, - block.PolishedGraniteStairs, - block.SmoothRedSandstoneStairs, - block.MossyStoneBrickStairs, - block.PolishedDioriteStairs, - block.MossyCobblestoneStairs, - block.EndStoneBrickStairs, - block.StoneStairs, - block.SmoothSandstoneStairs, - block.SmoothQuartzStairs, - block.GraniteStairs, - block.AndesiteStairs, - block.RedNetherBrickStairs, - block.PolishedAndesiteStairs, - block.OakSlab, - block.AcaciaSlab, - block.DarkOakSlab, - block.RedSandstoneSlab, - block.PolishedGraniteSlab, - block.SmoothRedSandstoneSlab, - block.MossyStoneBrickSlab, - block.PolishedDioriteSlab, - block.MossyCobblestoneSlab, - block.EndStoneBrickSlab, - block.StoneSlab, - block.SmoothSandstoneSlab, - block.SmoothQuartzSlab, - block.GraniteSlab, - block.AndesiteSlab, - block.RedNetherBrickSlab, - block.PolishedAndesiteSlab, - } - - slabs = map[block.ID]struct{}{ - block.OakSlab.ID: struct{}{}, - block.AcaciaSlab.ID: struct{}{}, - block.DarkOakSlab.ID: struct{}{}, - block.RedSandstoneSlab.ID: struct{}{}, - block.PolishedGraniteSlab.ID: struct{}{}, - block.SmoothRedSandstoneSlab.ID: struct{}{}, - block.MossyStoneBrickSlab.ID: struct{}{}, - block.PolishedDioriteSlab.ID: struct{}{}, - block.MossyCobblestoneSlab.ID: struct{}{}, - block.EndStoneBrickSlab.ID: struct{}{}, - block.StoneSlab.ID: struct{}{}, - block.SmoothSandstoneSlab.ID: struct{}{}, - block.SmoothQuartzSlab.ID: struct{}{}, - block.GraniteSlab.ID: struct{}{}, - block.AndesiteSlab.ID: struct{}{}, - block.RedNetherBrickSlab.ID: struct{}{}, - block.PolishedAndesiteSlab.ID: struct{}{}, - } - stairs = map[block.ID]struct{}{ - block.OakStairs.ID: struct{}{}, - block.AcaciaStairs.ID: struct{}{}, - block.DarkOakStairs.ID: struct{}{}, - block.RedSandstoneStairs.ID: struct{}{}, - block.PolishedGraniteStairs.ID: struct{}{}, - block.SmoothRedSandstoneStairs.ID: struct{}{}, - block.MossyStoneBrickStairs.ID: struct{}{}, - block.PolishedDioriteStairs.ID: struct{}{}, - block.MossyCobblestoneStairs.ID: struct{}{}, - block.EndStoneBrickStairs.ID: struct{}{}, - block.StoneStairs.ID: struct{}{}, - block.SmoothSandstoneStairs.ID: struct{}{}, - block.SmoothQuartzStairs.ID: struct{}{}, - block.GraniteStairs.ID: struct{}{}, - block.AndesiteStairs.ID: struct{}{}, - block.RedNetherBrickStairs.ID: struct{}{}, - block.PolishedAndesiteStairs.ID: struct{}{}, - } - - safeWalkBlocks = make(map[world.BlockStatus]struct{}, 128) - walkBlocks = []block.Block{ - block.Air, - block.CaveAir, - block.Grass, - block.Torch, - block.OakSign, - block.SpruceSign, - block.BirchSign, - block.AcaciaSign, - block.JungleSign, - block.DarkOakSign, - block.OakWallSign, - block.SpruceWallSign, - block.BirchWallSign, - block.AcaciaWallSign, - block.JungleWallSign, - block.DarkOakWallSign, - block.Cornflower, - block.TallGrass, - } - - additionalCostBlocks = map[*block.Block]int{ - &block.Rail: 120, - &block.PoweredRail: 200, - } -) - -func init() { - for _, b := range stepBlocks { - if b.MinStateID == b.MaxStateID { - safeStepBlocks[world.BlockStatus(b.MinStateID)] = struct{}{} - } else { - for i := b.MinStateID; i <= b.MaxStateID; i++ { - safeStepBlocks[world.BlockStatus(i)] = struct{}{} - } - } - } - - for _, b := range walkBlocks { - if b.MinStateID == b.MaxStateID { - safeWalkBlocks[world.BlockStatus(b.MinStateID)] = struct{}{} - } else { - for i := b.MinStateID; i <= b.MaxStateID; i++ { - safeWalkBlocks[world.BlockStatus(i)] = struct{}{} - } - } - } -} - -func SteppableBlock(bID world.BlockStatus) bool { - _, ok := safeStepBlocks[bID] - return ok -} - -func AirLikeBlock(bID world.BlockStatus) bool { - _, ok := safeWalkBlocks[bID] - return ok -} - -func IsLadder(bID world.BlockStatus) bool { - return uint32(bID) >= block.Ladder.MinStateID && uint32(bID) <= block.Ladder.MaxStateID -} - -func IsSlab(bID world.BlockStatus) bool { - _, isSlab := slabs[block.StateID[uint32(bID)]] - return isSlab -} diff --git a/bot/path/inputs.go b/bot/path/inputs.go deleted file mode 100644 index 2190ada..0000000 --- a/bot/path/inputs.go +++ /dev/null @@ -1,10 +0,0 @@ -package path - -// Inputs describes the desired movements of the player. -type Inputs struct { - Yaw, Pitch float64 - - ThrottleX, ThrottleZ float64 - - Jump bool -} diff --git a/bot/path/movement.go b/bot/path/movement.go deleted file mode 100644 index ed14cfc..0000000 --- a/bot/path/movement.go +++ /dev/null @@ -1,362 +0,0 @@ -package path - -import ( - "fmt" - - "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/data/block" -) - -// Cardinal directions. -type Direction uint8 - -func (d Direction) Offset() (x, y, z int) { - switch d { - case North: - return 0, 0, -1 - case South: - return 0, 0, 1 - case East: - return 1, 0, 0 - case West: - return -1, 0, 0 - } - panic(fmt.Sprintf("unknown direction value: %v", d)) -} - -func (d Direction) Offset2x() (x, y, z int) { - x, y, z = d.Offset() - return x + x, y + y, z + z -} - -func (d Direction) String() string { - switch d { - case North: - return "north" - case South: - return "south" - case East: - return "east" - case West: - return "west" - } - return fmt.Sprintf("Direction?<%d>", int(d)) -} - -// Valid direction values. -const ( - North Direction = iota - South - West - East -) - -func LadderDirection(bStateID world.BlockStatus) Direction { - return Direction(((uint32(bStateID) - block.Ladder.MinStateID) & 0xE) >> 1) -} - -func ChestDirection(bStateID world.BlockStatus) Direction { - return Direction(((uint32(bStateID) - block.Chest.MinStateID) / 6) & 0x3) -} - -func StairsDirection(bStateID world.BlockStatus) Direction { - b := block.StateID[uint32(bStateID)] - return Direction(((uint32(bStateID) - block.ByID[b].MinStateID) / 20) & 0x3) -} - -func SlabIsBottom(bStateID world.BlockStatus) bool { - b := block.StateID[uint32(bStateID)] - return ((uint32(bStateID)-block.ByID[b].MinStateID)&0xE)>>1 == 1 -} - -// Movement represents a single type of movement in a path. -type Movement uint8 - -var allMovements = []Movement{TraverseNorth, TraverseSouth, TraverseEast, TraverseWest, - TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast, - DropNorth, DropSouth, DropEast, DropWest, - Drop2North, Drop2South, Drop2East, Drop2West, - AscendNorth, AscendSouth, AscendEast, AscendWest, - DescendLadder, DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest, - AscendLadder, - JumpCrossNorth, JumpCrossSouth, JumpCrossEast, JumpCrossWest, -} - -// Valid movement values. -const ( - Waypoint Movement = iota - TraverseNorth - TraverseSouth - TraverseEast - TraverseWest - TraverseNorthEast - TraverseNorthWest - TraverseSouthEast - TraverseSouthWest - DropNorth - DropSouth - DropEast - DropWest - Drop2North - Drop2South - Drop2East - Drop2West - AscendNorth - AscendSouth - AscendEast - AscendWest - DescendLadder - DescendLadderNorth - DescendLadderSouth - DescendLadderEast - DescendLadderWest - AscendLadder - JumpCrossEast - JumpCrossWest - JumpCrossNorth - JumpCrossSouth -) - -func (m Movement) Possible(nav *Nav, x, y, z int, from V3, previous Movement) bool { - switch m { - case Waypoint, TraverseNorth, TraverseSouth, TraverseEast, TraverseWest: - if !SteppableBlock(nav.World.GetBlockStatus(x, y, z)) { - return false - } - b1, b2 := nav.World.GetBlockStatus(x, y+1, z), nav.World.GetBlockStatus(x, y+2, z) - u1, u2 := AirLikeBlock(b1) || IsLadder(b1), AirLikeBlock(b2) || IsLadder(b2) - return u1 && u2 - - case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast: - if !SteppableBlock(nav.World.GetBlockStatus(x, y, z)) { - return false - } - if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) { - return false - } - if !AirLikeBlock(nav.World.GetBlockStatus(from.X, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(from.X, y+2, z)) { - return false - } - return AirLikeBlock(nav.World.GetBlockStatus(x, y+1, from.Z)) && AirLikeBlock(nav.World.GetBlockStatus(x, y+2, from.Z)) - - case Drop2North, Drop2South, Drop2East, Drop2West: - if !AirLikeBlock(nav.World.GetBlockStatus(x, y+4, z)) { - return false - } - fallthrough - - case DropNorth, DropSouth, DropEast, DropWest: - for amt := 0; amt < 3; amt++ { - if !AirLikeBlock(nav.World.GetBlockStatus(x, y+amt+1, z)) { - return false - } - } - return SteppableBlock(nav.World.GetBlockStatus(x, y, z)) - - case AscendNorth, AscendSouth, AscendEast, AscendWest: - if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) { - return false - } - return SteppableBlock(nav.World.GetBlockStatus(x, y, z)) && - AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+1, from.Z)) && - AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+2, from.Z)) - - case DescendLadder, AscendLadder: - if bID := nav.World.GetBlockStatus(x, y+1, z); !AirLikeBlock(bID) && !IsLadder(bID) { - return false - } - return IsLadder(nav.World.GetBlockStatus(x, y, z)) - - case DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest: - for amt := 0; amt < 2; amt++ { - if bID := nav.World.GetBlockStatus(x, y+amt+1, z); !AirLikeBlock(bID) && !IsLadder(bID) { - return false - } - } - return IsLadder(nav.World.GetBlockStatus(x, y, z)) - - case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth: - if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+3, z)) { - return false - } - midX, midZ := (from.X+x)/2, (from.Z+z)/2 - if !AirLikeBlock(nav.World.GetBlockStatus(midX, y+1, midZ)) || !AirLikeBlock(nav.World.GetBlockStatus(midX, y+2, midZ)) || !AirLikeBlock(nav.World.GetBlockStatus(midX, y+3, midZ)) { - return false - } - if !AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+3, from.Z)) { - return false - } - return SteppableBlock(nav.World.GetBlockStatus(x, y, z)) - - default: - panic(m) - } -} - -func (m Movement) Offset() (x, y, z int) { - switch m { - case Waypoint: - return 0, 0, 0 - case TraverseNorth: - return North.Offset() - case TraverseSouth: - return South.Offset() - case TraverseEast: - return East.Offset() - case TraverseWest: - return West.Offset() - - case JumpCrossEast: - return East.Offset2x() - case JumpCrossWest: - return West.Offset2x() - case JumpCrossNorth: - return North.Offset2x() - case JumpCrossSouth: - return South.Offset2x() - - case AscendLadder: - return 0, 1, 0 - case DescendLadder: - return 0, -1, 0 - case DropNorth, DescendLadderNorth: - return 0, -1, -1 - case DropSouth, DescendLadderSouth: - return 0, -1, 1 - case DropEast, DescendLadderEast: - return 1, -1, 0 - case DropWest, DescendLadderWest: - return -1, -1, 0 - case AscendNorth: - return 0, 1, -1 - case AscendSouth: - return 0, 1, 1 - case AscendEast: - return 1, 1, 0 - case AscendWest: - return -1, 1, 0 - - case TraverseNorthWest: - return -1, 0, -1 - case TraverseNorthEast: - return 1, 0, -1 - case TraverseSouthWest: - return -1, 0, 1 - case TraverseSouthEast: - return 1, 0, 1 - - case Drop2North: - return 0, -2, -1 - case Drop2South: - return 0, -2, 1 - case Drop2East: - return 1, -2, 0 - case Drop2West: - return -1, -2, 0 - - default: - panic(m) - } -} - -func (m Movement) BaseCost() float64 { - switch m { - case Waypoint: - return 0 - case TraverseNorth, TraverseSouth, TraverseEast, TraverseWest: - return 2 - case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast: - return 2.5 - - case DropNorth, DropSouth, DropEast, DropWest, Drop2North, Drop2South, Drop2East, Drop2West: - return 4 - case AscendNorth, AscendSouth, AscendEast, AscendWest: - return 4 - case DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest: - return 1.5 - case DescendLadder: - return 1 - case AscendLadder: - return 3 - - case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth: - return 5.5 - default: - panic(m) - } -} - -func (m Movement) String() string { - switch m { - case Waypoint: - return "waypoint" - case TraverseNorth: - return "traverse-north" - case TraverseSouth: - return "traverse-south" - case TraverseEast: - return "traverse-east" - case TraverseWest: - return "traverse-west" - - case DropNorth: - return "drop-north" - case DropSouth: - return "drop-south" - case DropEast: - return "drop-east" - case DropWest: - return "drop-west" - case Drop2North: - return "drop-2-north" - case Drop2South: - return "drop-2-south" - case Drop2East: - return "drop-2-east" - case Drop2West: - return "drop-2-west" - - case AscendNorth: - return "jump-north" - case AscendSouth: - return "jump-south" - case AscendEast: - return "jump-east" - case AscendWest: - return "jump-west" - - case TraverseNorthWest: - return "traverse-northwest" - case TraverseNorthEast: - return "traverse-northeast" - case TraverseSouthWest: - return "traverse-southwest" - case TraverseSouthEast: - return "traverse-southeast" - - case DescendLadder: - return "descend-ladder" - case DescendLadderNorth: - return "descend-ladder-north" - case DescendLadderSouth: - return "descend-ladder-south" - case DescendLadderEast: - return "descend-ladder-east" - case DescendLadderWest: - return "descend-ladder-west" - - case AscendLadder: - return "ascend-ladder" - - case JumpCrossEast: - return "jump-crossing-east" - case JumpCrossWest: - return "jump-crossing-west" - case JumpCrossNorth: - return "jump-crossing-north" - case JumpCrossSouth: - return "jump-crossing-south" - default: - panic(m) - } -} diff --git a/bot/path/path.go b/bot/path/path.go deleted file mode 100644 index d860ad6..0000000 --- a/bot/path/path.go +++ /dev/null @@ -1,208 +0,0 @@ -// Package path implements pathfinding. -package path - -import ( - "math" - "math/rand" - "time" - - "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/data/block" - "github.com/beefsack/go-astar" -) - -// Point represents a point in 3D space. -type Point struct { - X, Y, Z float64 -} - -type V3 struct { - X, Y, Z int -} - -func (v V3) Cost(other V3) float64 { - x, y, z := v.X-other.X, v.Y-other.Y, v.Z-other.Z - return math.Sqrt(float64(x*x+z*z)) + math.Sqrt(1.2*float64(y*y)) -} - -// Nav represents a navigation to a position. -type Nav struct { - World *world.World - Start, Dest V3 -} - -func (n *Nav) Path() (path []astar.Pather, distance float64, found bool) { - return astar.Path( - Tile{ // Start point - Nav: n, - Movement: Waypoint, - Pos: n.Start, - }, - Tile{ // Destination point - Nav: n, - Movement: Waypoint, - Pos: n.Dest, - }) -} - -// Tile represents a point in a path. All tiles in a path are adjaceent their -// preceeding tiles. -type Tile struct { - Nav *Nav - - HalfBlock bool - Movement Movement - Pos V3 - BlockStatus world.BlockStatus - ExtraCost int -} - -func (t Tile) PathNeighborCost(to astar.Pather) float64 { - other := to.(Tile) - return 1 + other.Movement.BaseCost() -} - -func (t Tile) PathEstimatedCost(to astar.Pather) float64 { - other := to.(Tile) - cost := t.Pos.Cost(other.Pos) - - return cost + other.Movement.BaseCost() -} - -func (t Tile) PathNeighbors() []astar.Pather { - possibles := make([]astar.Pather, 0, 8) - - if c := t.PathEstimatedCost(Tile{Pos: t.Nav.Start}); c > 8000 { - return nil - } - - if t.Pos == t.Nav.Dest && t.Movement != Waypoint { - dupe := t - dupe.Movement = Waypoint - dupe.BlockStatus = 0 - return []astar.Pather{dupe} - } - - for _, m := range allMovements { - x, y, z := m.Offset() - pos := V3{X: t.Pos.X + x, Y: t.Pos.Y + y, Z: t.Pos.Z + z} - possible := m.Possible(t.Nav, pos.X, pos.Y, pos.Z, t.Pos, t.Movement) - if possible { - bStateID := t.Nav.World.GetBlockStatus(pos.X, pos.Y, pos.Z) - possibles = append(possibles, Tile{ - Nav: t.Nav, - Movement: m, - Pos: pos, - BlockStatus: bStateID, - HalfBlock: IsSlab(bStateID) && SlabIsBottom(bStateID), - }) - } - } - - return possibles -} - -func (t Tile) Inputs(pos, deltaPos, vel Point, runTime time.Duration) Inputs { - // Sufficient for simple movements. - at := math.Atan2(-deltaPos.X, -deltaPos.Z) - mdX, _, mdZ := t.Movement.Offset() - wantYaw := -math.Atan2(float64(mdX), float64(mdZ)) * 180 / math.Pi - out := Inputs{ - ThrottleX: math.Sin(at), - ThrottleZ: math.Cos(at), - Yaw: wantYaw, - } - if mdX == 0 && mdZ == 0 { - out.Yaw = math.NaN() - } - if (rand.Int() % 14) == 0 { - out.Pitch = float64((rand.Int() % 4) - 2) - } - - switch t.Movement { - case DescendLadder, DescendLadderEast, DescendLadderWest, DescendLadderNorth, DescendLadderSouth: - // Deadzone the throttle to prevent an accidental ascend. - if dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z); dist2 < (0.22 * 0.22 * 2) { - out.ThrottleX, out.ThrottleZ = 0, 0 - } - - case AscendLadder: - dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z) - - if x, _, z := LadderDirection(t.BlockStatus).Offset(); dist2 > (0.8*0.8) && deltaPos.Y < 0 { - pos.X -= (0.25 * float64(x)) - pos.Z -= (0.25 * float64(z)) - } else { - pos.X += (0.42 * float64(x)) - pos.Z += (0.42 * float64(z)) - } - - at = math.Atan2(-pos.X+float64(t.Pos.X)+0.5, -pos.Z+float64(t.Pos.Z)+0.5) - out = Inputs{ - ThrottleX: math.Sin(at), - ThrottleZ: math.Cos(at), - Yaw: math.NaN(), - } - - case AscendNorth, AscendSouth, AscendEast, AscendWest: - var ( - b = block.ByID[block.StateID[uint32(t.BlockStatus)]] - _, isStairs = stairs[b.ID] - _, isSlab = slabs[b.ID] - maybeStuck = runTime < 1250*time.Millisecond - dist2 = math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z) - ) - out.Jump = dist2 < 1.75 && deltaPos.Y < -0.81 - - switch { - case isStairs: - // Special logic for stairs: Try to go towards the downwards edge initially. - if dist2 > (0.9*0.9) && deltaPos.Y < 0 { - if x, _, z := StairsDirection(t.BlockStatus).Offset(); dist2 > (0.9*0.9) && deltaPos.Y < 0 { - pos.X += (0.49 * float64(x)) - pos.Z += (0.49 * float64(z)) - } - - at = math.Atan2(-pos.X+float64(t.Pos.X)+0.5, -pos.Z+float64(t.Pos.Z)+0.5) - out = Inputs{ - ThrottleX: math.Sin(at), - ThrottleZ: math.Cos(at), - Yaw: math.NaN(), - Jump: out.Jump && !maybeStuck, - } - } - // We dont need to jump for slabs, so only jump if we get stuck. - case isSlab: - out.Jump = out.Jump && !maybeStuck - } - - // Turn off the throttle if we get stuck on the jump. - if dist2 < 1 && deltaPos.Y < 0 && vel.Y == 0 { - out.ThrottleX, out.ThrottleZ = 0, 0 - } - - case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth: - dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z) - out.Jump = dist2 > 1.5 && dist2 < 1.78 - } - return out -} - -func (t Tile) IsComplete(d Point) bool { - switch t.Movement { - case DescendLadder, DescendLadderNorth, DescendLadderSouth, DescendLadderWest, DescendLadderEast, - DropNorth, DropSouth, DropEast, DropWest: - return (d.X*d.X+d.Z*d.Z) < (2*0.2*0.25) && d.Y <= 0.05 - case AscendLadder: - return d.Y >= 0 - case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth: - return (d.X*d.X+d.Z*d.Z) < (0.22*0.22) && d.Y >= -0.065 - } - - yLowerCutoff := -0.065 - if t.HalfBlock { - yLowerCutoff -= 0.5 - } - - return (d.X*d.X+d.Z*d.Z) < (0.18*0.18) && d.Y >= yLowerCutoff && d.Y <= 0.08 -} diff --git a/bot/phy/aabb.go b/bot/phy/aabb.go deleted file mode 100644 index 513543c..0000000 --- a/bot/phy/aabb.go +++ /dev/null @@ -1,143 +0,0 @@ -package phy - -import ( - "math" - - "github.com/Tnze/go-mc/bot/world" -) - -type MinMax struct { - Min, Max float64 -} - -// Extends adjusts the bounds of the MinMax. A negative number will reduce the -// minimum bound, whereas a positive number will increase the maximum bound. -func (mm MinMax) Extend(delta float64) MinMax { - if delta < 0 { - return MinMax{ - Min: mm.Min + delta, - Max: mm.Max, - } - } - - return MinMax{ - Min: mm.Min, - Max: mm.Max + delta, - } -} - -// Contract reduces both the minimum and maximum bound by the provided amount, -// such that the difference between the bounds decreases for positive values. -func (mm MinMax) Contract(amt float64) MinMax { - return MinMax{ - Min: mm.Min + amt, - Max: mm.Max - amt, - } -} - -// Expand changes the minimum and maximum bounds by the provided amount, such -// that the difference between the bounds increases for positive values. -func (mm MinMax) Expand(amt float64) MinMax { - return MinMax{ - Min: mm.Min - amt, - Max: mm.Max + amt, - } -} - -// Offset adds the provided value to both the minimum and maximum value. -func (mm MinMax) Offset(amt float64) MinMax { - return MinMax{ - Min: mm.Min + amt, - Max: mm.Max + amt, - } -} - -// AABB implements Axis Aligned Bounding Box operations. -type AABB struct { - X, Y, Z MinMax - Block world.BlockStatus -} - -// Extend adjusts the minimum (for negative values) or maximum bounds (for -// positive values) by the provided scalar for each dimension. -func (bb AABB) Extend(dx, dy, dz float64) AABB { - return AABB{ - X: bb.X.Extend(dx), - Y: bb.Y.Extend(dx), - Z: bb.Z.Extend(dx), - Block: bb.Block, - } -} - -// Contract reduces the difference between the min/max bounds (for positive -// values) for each dimension. -func (bb AABB) Contract(x, y, z float64) AABB { - return AABB{ - X: bb.X.Contract(x), - Y: bb.Y.Contract(y), - Z: bb.Z.Contract(z), - Block: bb.Block, - } -} - -// Expand increases both the minimum and maximum bounds by the provided amount -// (for positive values) for each dimension. -func (bb AABB) Expand(x, y, z float64) AABB { - return AABB{ - X: bb.X.Expand(x), - Y: bb.Y.Expand(y), - Z: bb.Z.Expand(z), - Block: bb.Block, - } -} - -// Offset moves both the minimum and maximum bound by the provided value for -// each dimension. -func (bb AABB) Offset(x, y, z float64) AABB { - return AABB{ - X: bb.X.Offset(x), - Y: bb.Y.Offset(y), - Z: bb.Z.Offset(z), - Block: bb.Block, - } -} - -func (bb AABB) XOffset(o AABB, xOffset float64) float64 { - if o.Y.Max > bb.Y.Min && o.Y.Min < bb.Y.Max && o.Z.Max > bb.Z.Min && o.Z.Min < bb.Z.Max { - if xOffset > 0.0 && o.X.Max <= bb.X.Min { - xOffset = math.Min(bb.X.Min-o.X.Max, xOffset) - } else if xOffset < 0.0 && o.X.Min >= bb.X.Max { - xOffset = math.Max(bb.X.Max-o.X.Min, xOffset) - } - } - return xOffset -} - -func (bb AABB) YOffset(o AABB, yOffset float64) float64 { - if o.X.Max > bb.X.Min && o.X.Min < bb.X.Max && o.Z.Max > bb.Z.Min && o.Z.Min < bb.Z.Max { - if yOffset > 0.0 && o.Y.Max <= bb.Y.Min { - yOffset = math.Min(bb.Y.Min-o.Y.Max, yOffset) - } else if yOffset < 0.0 && o.Y.Min >= bb.Y.Max { - yOffset = math.Max(bb.Y.Max-o.Y.Min, yOffset) - } - } - return yOffset -} - -func (bb AABB) ZOffset(o AABB, zOffset float64) float64 { - if o.X.Max > bb.X.Min && o.X.Min < bb.X.Max && o.Y.Max > bb.Y.Min && o.Y.Min < bb.Y.Max { - if zOffset > 0.0 && o.Z.Max <= bb.Z.Min { - zOffset = math.Min(bb.Z.Min-o.Z.Max, zOffset) - } else if zOffset < 0.0 && o.Z.Min >= bb.Z.Max { - zOffset = math.Max(bb.Z.Max-o.Z.Min, zOffset) - } - } - return zOffset -} - -func (bb AABB) Intersects(o AABB) bool { - return true && - bb.X.Min < o.X.Max && bb.X.Max > o.X.Min && - bb.Y.Min < o.Y.Max && bb.Y.Max > o.Y.Min && - bb.Z.Min < o.Z.Max && bb.Z.Max > o.Z.Min -} diff --git a/bot/phy/phy.go b/bot/phy/phy.go deleted file mode 100644 index d0d8e96..0000000 --- a/bot/phy/phy.go +++ /dev/null @@ -1,288 +0,0 @@ -// Package phy implements a minimal physics simulation necessary for realistic -// bot behavior. -package phy - -import ( - "math" - - "github.com/Tnze/go-mc/bot/path" - "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/bot/world/entity/player" - "github.com/Tnze/go-mc/data/block/shape" -) - -const ( - playerWidth = 0.6 - playerHeight = 1.8 - resetVel = 0.003 - - maxYawChange = 11 - maxPitchChange = 7 - - stepHeight = 0.6 - minJumpTicks = 14 - ladderMaxSpeed = 0.15 - ladderClimbSpeed = 0.2 - - gravity = 0.08 - drag = 0.98 - acceleration = 0.02 - inertia = 0.91 - slipperiness = 0.6 -) - -// World represents a provider of information about the surrounding world. -type World interface { - GetBlockStatus(x, y, z int) world.BlockStatus -} - -// Surrounds represents the blocks surrounding the player (Y, Z, X). -type Surrounds []AABB - -// State tracks physics state. -type State struct { - // player state. - Pos path.Point - Vel path.Point - Yaw, Pitch float64 - lastJump uint32 - - // player state flags. - onGround bool - collision struct { - vertical bool - horizontal bool - } - - tick uint32 - Run bool -} - -func (s *State) ServerPositionUpdate(player player.Pos, w World) error { - s.Pos = path.Point{X: player.X, Y: player.Y, Z: player.Z} - s.Yaw, s.Pitch = float64(player.Yaw), float64(player.Pitch) - s.Vel = path.Point{} - s.onGround, s.collision.vertical, s.collision.horizontal = false, false, false - s.Run = true - return nil -} - -func abs(i1, i2 int) int { - if i1 < i2 { - return i2 - i1 - } - return i1 - i2 -} - -func (s *State) surroundings(query AABB, w World) Surrounds { - minY, maxY := int(math.Floor(query.Y.Min))-1, int(math.Floor(query.Y.Max))+1 - minZ, maxZ := int(math.Floor(query.Z.Min)), int(math.Floor(query.Z.Max))+1 - minX, maxX := int(math.Floor(query.X.Min)), int(math.Floor(query.X.Max))+1 - - out := Surrounds(make([]AABB, 0, abs(maxY, minY)*abs(maxZ, minZ)*abs(maxX, minX)*2)) - for y := minY; y < maxY; y++ { - for z := minZ; z < maxZ; z++ { - for x := minX; x < maxX; x++ { - bStateID := w.GetBlockStatus(x, y, z) - if !path.AirLikeBlock(bStateID) { - bbs, err := shape.CollisionBoxes(bStateID) - if err != nil { - panic(err) - } - for _, box := range bbs { - out = append(out, AABB{ - X: MinMax{Min: box.Min.X, Max: box.Max.X}, - Y: MinMax{Min: box.Min.Y, Max: box.Max.Y}, - Z: MinMax{Min: box.Min.Z, Max: box.Max.Z}, - Block: bStateID, - }.Offset(float64(x), float64(y), float64(z))) - } - } - } - } - } - return out -} - -func (s *State) BB() AABB { - return AABB{ - X: MinMax{Min: -playerWidth / 2, Max: playerWidth / 2}, - Y: MinMax{Max: playerHeight}, - Z: MinMax{Min: -playerWidth / 2, Max: playerWidth / 2}, - }.Offset(s.Pos.X, s.Pos.Y, s.Pos.Z) -} - -func (s *State) Position() player.Pos { - return player.Pos{ - X: s.Pos.X, Y: s.Pos.Y, Z: s.Pos.Z, - Yaw: float32(s.Yaw), Pitch: float32(s.Pitch), - OnGround: s.onGround, - } -} - -func (s *State) Tick(input path.Inputs, w World) error { - s.tick++ - if !s.Run { - return nil - } - - var inertia = inertia - var acceleration = acceleration - if below := w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y))-1, int(math.Floor(s.Pos.Z))); s.onGround && !path.AirLikeBlock(below) { - inertia *= slipperiness - acceleration = 0.1 * (0.1627714 / (inertia * inertia * inertia)) - } - - s.tickVelocity(input, inertia, acceleration, w) - s.tickPosition(w) - - if path.IsLadder(w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y)), int(math.Floor(s.Pos.Z)))) && s.collision.horizontal { - s.Vel.Y = ladderClimbSpeed - } - - // Gravity - s.Vel.Y -= gravity - // Drag & friction. - s.Vel.Y *= drag - s.Vel.X *= inertia - s.Vel.Z *= inertia - return nil -} - -func (s *State) tickVelocity(input path.Inputs, inertia, acceleration float64, w World) { - - // Deadzone velocities when they get too low. - if math.Abs(s.Vel.X) < resetVel { - s.Vel.X = 0 - } - if math.Abs(s.Vel.Y) < resetVel { - s.Vel.Y = 0 - } - if math.Abs(s.Vel.Z) < resetVel { - s.Vel.Z = 0 - } - - s.applyLookInputs(input) - s.applyPosInputs(input, acceleration, inertia) - - lower := w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y)), int(math.Floor(s.Pos.Z))) - if path.IsLadder(lower) { - s.Vel.X = math.Min(math.Max(-ladderMaxSpeed, s.Vel.X), ladderMaxSpeed) - s.Vel.Z = math.Min(math.Max(-ladderMaxSpeed, s.Vel.Z), ladderMaxSpeed) - s.Vel.Y = math.Min(math.Max(-ladderMaxSpeed, s.Vel.Y), ladderMaxSpeed) - } -} - -func (s *State) applyLookInputs(input path.Inputs) { - if !math.IsNaN(input.Yaw) { - errYaw := math.Min(math.Max(modYaw(input.Yaw, s.Yaw), -maxYawChange), maxYawChange) - s.Yaw += errYaw - } - errPitch := math.Min(math.Max(input.Pitch-s.Pitch, -maxPitchChange), maxPitchChange) - s.Pitch += errPitch -} - -func (s *State) applyPosInputs(input path.Inputs, acceleration, inertia float64) { - if input.Jump && s.lastJump+minJumpTicks < s.tick && s.onGround { - s.lastJump = s.tick - s.Vel.Y = 0.42 - } - - speed := math.Sqrt(input.ThrottleX*input.ThrottleX + input.ThrottleZ*input.ThrottleZ) - if speed < 0.01 { - return - } - speed = acceleration / math.Max(speed, 1) - - input.ThrottleX *= speed - input.ThrottleZ *= speed - - s.Vel.X += input.ThrottleX - s.Vel.Z += input.ThrottleZ -} - -func (s *State) tickPosition(w World) { - p, newVel := s.computeCollisionYXZ(s.BB(), s.BB().Offset(s.Vel.X, s.Vel.Y, s.Vel.Z), s.Vel, w) - - if s.onGround || (s.Vel.Y != newVel.Y && s.Vel.Y < 0) { - bb := s.BB() - surroundings := s.surroundings(bb.Offset(s.Vel.X, stepHeight, s.Vel.Z), w) - outVel := s.Vel - - outVel.Y = stepHeight - for _, b := range surroundings { - outVel.Y = b.YOffset(bb, outVel.Y) - } - bb = bb.Offset(0, outVel.Y, 0) - for _, b := range surroundings { - outVel.X = b.XOffset(bb, outVel.X) - } - bb = bb.Offset(outVel.X, 0, 0) - for _, b := range surroundings { - outVel.Z = b.ZOffset(bb, outVel.Z) - } - bb = bb.Offset(0, 0, outVel.Z) - - outVel.Y *= -1 - // Lower the player back down to be on the ground. - for _, b := range surroundings { - outVel.Y = b.YOffset(bb, outVel.Y) - } - bb = bb.Offset(0, outVel.Y, 0) - - oldMove := newVel.X*newVel.X + newVel.Z*newVel.Z - newMove := outVel.X*outVel.X + outVel.Z*outVel.Z - - if oldMove >= newMove || outVel.Y <= (0.000002-stepHeight) { - // fmt.Println("nope") - } else { - p = bb - newVel = outVel - } - } - - // Update flags. - s.Pos.X = p.X.Min + playerWidth/2 - s.Pos.Y = p.Y.Min - s.Pos.Z = p.Z.Min + playerWidth/2 - s.collision.horizontal = newVel.X != s.Vel.X || newVel.Z != s.Vel.Z - s.collision.vertical = newVel.Y != s.Vel.Y - s.onGround = s.collision.vertical && s.Vel.Y < 0 - s.Vel = newVel -} - -func modYaw(new, old float64) float64 { - delta := math.Mod(new-old, 360) - if delta > 180 { - delta = 180 - delta - } else if delta < -180 { - delta += 360 - } - return delta -} - -func (s *State) computeCollisionYXZ(bb, query AABB, vel path.Point, w World) (outBB AABB, outVel path.Point) { - surroundings := s.surroundings(query, w) - outVel = vel - - for _, b := range surroundings { - outVel.Y = b.YOffset(bb, outVel.Y) - } - bb = bb.Offset(0, outVel.Y, 0) - for _, b := range surroundings { - outVel.X = b.XOffset(bb, outVel.X) - } - bb = bb.Offset(outVel.X, 0, 0) - for _, b := range surroundings { - outVel.Z = b.ZOffset(bb, outVel.Z) - } - bb = bb.Offset(0, 0, outVel.Z) - return bb, outVel -} - -// AtLookTarget returns true if the player look position is actually at the -// given pitch and yaw. -func (s *State) AtLookTarget(yaw, pitch float64) bool { - dYaw, dPitch := math.Abs(modYaw(yaw, s.Yaw)), math.Abs(pitch-s.Pitch) - return dYaw <= 0.8 && dPitch <= 1.1 -} diff --git a/bot/pinglist.go b/bot/pinglist.go index 1fb08b9..c8fff63 100644 --- a/bot/pinglist.go +++ b/bot/pinglist.go @@ -2,9 +2,12 @@ package bot import ( "fmt" + "net" + "strconv" "time" - "github.com/Tnze/go-mc/net" + "github.com/Tnze/go-mc/data/packetid" + mcnet "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" ) @@ -12,49 +15,70 @@ import ( // Returns a JSON data with server status, and the delay. // // For more information for JSON format, see https://wiki.vg/Server_List_Ping#Response -func PingAndList(addr string, port int) ([]byte, time.Duration, error) { - conn, err := net.DialMC(fmt.Sprintf("%s:%d", addr, port)) +func PingAndList(addr string) ([]byte, time.Duration, error) { + addrSrv, err := parseAddress(&net.Resolver{}, addr) if err != nil { - return nil, 0, fmt.Errorf("bot: dial fail: %v", err) + return nil, 0, LoginErr{"parse address", err} } - return pingAndList(addr, port, conn) + + conn, err := mcnet.DialMC(addrSrv) + if err != nil { + return nil, 0, LoginErr{"dial connection", err} + } + return pingAndList(addr, conn) } // PingAndListTimeout PingAndLIstTimeout is the version of PingAndList with max request time. -func PingAndListTimeout(addr string, port int, timeout time.Duration) ([]byte, time.Duration, error) { +func PingAndListTimeout(addr string, timeout time.Duration) ([]byte, time.Duration, error) { deadLine := time.Now().Add(timeout) - conn, err := net.DialMCTimeout(fmt.Sprintf("%s:%d", addr, port), timeout) + addrSrv, err := parseAddress(&net.Resolver{}, addr) if err != nil { - return nil, 0, fmt.Errorf("bot: dial fail: %v", err) + return nil, 0, LoginErr{"parse address", err} + } + + conn, err := mcnet.DialMCTimeout(addrSrv, timeout) + if err != nil { + return nil, 0, err } err = conn.Socket.SetDeadline(deadLine) if err != nil { - return nil, 0, fmt.Errorf("bot: set deadline fail: %v", err) + return nil, 0, LoginErr{"set deadline", err} } - return pingAndList(addr, port, conn) + return pingAndList(addr, conn) } -func pingAndList(addr string, port int, conn *net.Conn) ([]byte, time.Duration, error) { +func pingAndList(addr string, conn *mcnet.Conn) ([]byte, time.Duration, error) { + // Split Host and Port + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return nil, 0, LoginErr{"split address", err} + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, 0, LoginErr{"parse port", err} + } + + const Handshake = 0x00 //握手 - err := conn.WritePacket( - //Handshake Packet - pk.Marshal( - 0x00, //Handshake packet ID - pk.VarInt(ProtocolVersion), //Protocol version - pk.String(addr), //Server's address - pk.UnsignedShort(port), - pk.Byte(1), - )) + err = conn.WritePacket(pk.Marshal( + Handshake, //Handshake packet ID + pk.VarInt(ProtocolVersion), //Protocol version + pk.String(host), //Server's address + pk.UnsignedShort(port), + pk.Byte(1), + )) if err != nil { return nil, 0, fmt.Errorf("bot: send handshake packect fail: %v", err) } //LIST //请求服务器状态 - err = conn.WritePacket(pk.Marshal(0)) + err = conn.WritePacket(pk.Marshal( + packetid.PingStart, + )) if err != nil { return nil, 0, fmt.Errorf("bot: send list packect fail: %v", err) } @@ -72,7 +96,10 @@ func pingAndList(addr string, port int, conn *net.Conn) ([]byte, time.Duration, //PING startTime := time.Now() - err = conn.WritePacket(pk.Marshal(0x01, pk.Long(startTime.Unix()))) + err = conn.WritePacket(pk.Marshal( + packetid.PingServerbound, + pk.Long(startTime.Unix()), + )) if err != nil { return nil, 0, fmt.Errorf("bot: send ping packect fail: %v", err) } diff --git a/bot/world/bitarray.go b/bot/world/bitarray.go deleted file mode 100644 index 5abbfb4..0000000 --- a/bot/world/bitarray.go +++ /dev/null @@ -1,38 +0,0 @@ -package world - -// bitArray implements a bitfield array where values are packed into uint64 -// values. If the next value does not fit into remaining space, the remaining -// space of a uint64 is unused. -type bitArray struct { - width uint // bit width of each value - valuesPerElement uint // number of values which fit into a single uint64. - - data []uint64 -} - -// Size returns the number of elements that can fit into the bit array. -func (b *bitArray) Size() int { - return int(b.valuesPerElement) * len(b.data) -} - -func (b *bitArray) Set(idx, val uint) { - var ( - arrayIdx = idx / b.valuesPerElement - startBit = (idx % b.valuesPerElement) * b.width - mask = ^uint64((1<> offset -} - -func valuesPerBitArrayElement(bitsPerValue uint) uint { - return uint(64 / bitsPerValue) -} diff --git a/bot/world/bitarray_test.go b/bot/world/bitarray_test.go deleted file mode 100644 index daa0d98..0000000 --- a/bot/world/bitarray_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package world - -import ( - "encoding/binary" - "encoding/hex" - "testing" -) - -func TestBitArrayBasic(t *testing.T) { - a := bitArray{ - width: 5, - valuesPerElement: valuesPerBitArrayElement(5), - data: make([]uint64, 5), - } - - if got, want := a.Size(), 12*5; got != want { - t.Errorf("size = %d, want %d", got, want) - } - - a.Set(0, 4) - if v := a.Get(0); v != 4 { - t.Errorf("v[0] = %d, want 4", v) - } - a.Set(12, 8) - if v := a.Get(12); v != 8 { - t.Errorf("v[12] = %d, want 8", v) - } -} - -func TestBitArrayHardcoded(t *testing.T) { - d1, _ := hex.DecodeString("0020863148418841") - d2, _ := hex.DecodeString("01018A7260F68C87") - - a := bitArray{ - width: 5, - valuesPerElement: valuesPerBitArrayElement(5), - data: []uint64{binary.BigEndian.Uint64(d1), binary.BigEndian.Uint64(d2)}, - } - - if got, want := a.Size(), 12*2; got != want { - t.Errorf("size = %d, want %d", got, want) - } - - want := []uint{1, 2, 2, 3, 4, 4, 5, 6, 6, 4, 8, 0, 7, 4, 3, 13, 15, 16, 9, 14, 10, 12, 0, 2} - for idx, want := range want { - if got := a.Get(uint(idx)); got != want { - t.Errorf("v[%d] = %d, want %d", idx, got, want) - } - } -} diff --git a/bot/world/chunk.go b/bot/world/chunk.go deleted file mode 100644 index c20d31a..0000000 --- a/bot/world/chunk.go +++ /dev/null @@ -1,173 +0,0 @@ -package world - -import ( - "bytes" - "fmt" - "io" - "math" - - "github.com/Tnze/go-mc/data/block" - pk "github.com/Tnze/go-mc/net/packet" -) - -const maxPaletteBits = 8 - -// DecodeChunkColumn decode the chunk data structure. -// If decoding went error, successful decoded data will be returned. -func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) { - var c Chunk - r := bytes.NewReader(data) - for sectionY := 0; sectionY < 16; sectionY++ { - // If the section's bit set in the mask - if (mask & (1 << uint(sectionY))) != 0 { - // read section - sec, err := readSection(r) - if err != nil { - return &c, fmt.Errorf("read section[%d] error: %w", sectionY, err) - } - c.Sections[sectionY] = sec - } - } - return &c, nil -} - -func perBits(bpb byte) uint { - switch { - case bpb <= 4: - return 4 - case bpb <= maxPaletteBits: - return uint(bpb) - default: - return uint(block.BitsPerBlock) - } -} - -func readSection(data io.Reader) (s Section, err error) { - var nonAirBlockCount pk.Short - if _, err := nonAirBlockCount.ReadFrom(data); err != nil { - return nil, fmt.Errorf("block count: %w", err) - } - var bpb pk.UnsignedByte - if _, err := bpb.ReadFrom(data); err != nil { - return nil, fmt.Errorf("bits per block: %w", err) - } - // If bpb values greater than or equal to 9, use directSection. - // Otherwise use paletteSection. - var palettes []BlockStatus - var palettesIndex map[BlockStatus]int - if bpb <= maxPaletteBits { - // read palettes - var length pk.VarInt - if _, err := length.ReadFrom(data); err != nil { - return nil, fmt.Errorf("palette length: %w", err) - } - palettes = make([]BlockStatus, length) - palettesIndex = make(map[BlockStatus]int, length) - for i := 0; i < int(length); i++ { - var v pk.VarInt - if _, err := v.ReadFrom(data); err != nil { - return nil, fmt.Errorf("read palettes[%d] error: %w", i, err) - } - palettes[i] = BlockStatus(v) - palettesIndex[BlockStatus(v)] = i - } - } - - // read data array - var dataLen pk.VarInt - if _, err := dataLen.ReadFrom(data); err != nil { - return nil, fmt.Errorf("read data array length error: %w", err) - } - if int(dataLen) < 16*16*16*int(bpb)/64 { - return nil, fmt.Errorf("data length (%d) is not enough of given bpb (%d)", dataLen, bpb) - } - dataArray := make([]uint64, dataLen) - for i := 0; i < int(dataLen); i++ { - var v pk.Long - if _, err := v.ReadFrom(data); err != nil { - return nil, fmt.Errorf("read dataArray[%d] error: %w", i, err) - } - dataArray[i] = uint64(v) - } - - width := perBits(byte(bpb)) - sec := directSection{ - bitArray{ - width: width, - valuesPerElement: valuesPerBitArrayElement(width), - data: dataArray, - }, - } - if bpb <= maxPaletteBits { - return &paletteSection{ - palette: palettes, - palettesIndex: palettesIndex, - directSection: sec, - }, nil - } else { - return &sec, nil - } -} - -type directSection struct { - bitArray -} - -func (d *directSection) GetBlock(offset uint) BlockStatus { - return BlockStatus(d.Get(offset)) -} - -func (d *directSection) SetBlock(offset uint, s BlockStatus) { - d.Set(offset, uint(s)) -} - -func (d *directSection) CanContain(s BlockStatus) bool { - return s <= (1<> 8) & 0xff), int((p >> 16) & 0xff), int(p & 0xff) -} - -func (p TilePosition) String() string { - x, y, z := p.Pos() - return fmt.Sprintf("(%d, %d, %d)", x, y, z) -} - -func ToTilePos(x, y, z int) TilePosition { - return TilePosition((y&0xff)<<16 | (x&15)<<8 | (z & 15)) -} diff --git a/bot/world/world.go b/bot/world/world.go deleted file mode 100644 index 9089233..0000000 --- a/bot/world/world.go +++ /dev/null @@ -1,15 +0,0 @@ -package world - -import ( - "sync" - - "github.com/Tnze/go-mc/bot/world/entity" -) - -// World record all of the things in the world where player at -type World struct { - entityLock sync.RWMutex - Entities map[int32]*entity.Entity - chunkLock sync.RWMutex - Chunks map[ChunkLoc]*Chunk -} diff --git a/bot/world/world_chunk.go b/bot/world/world_chunk.go deleted file mode 100644 index 4157e90..0000000 --- a/bot/world/world_chunk.go +++ /dev/null @@ -1,152 +0,0 @@ -package world - -import ( - "github.com/Tnze/go-mc/bot/world/entity" - "github.com/Tnze/go-mc/data/block" - pk "github.com/Tnze/go-mc/net/packet" - "github.com/Tnze/go-mc/net/ptypes" -) - -// Chunk store a 256*16*16 area of blocks, sharded on the Y axis into 16 -// sections. -type Chunk struct { - Sections [16]Section - TileEntities map[TilePosition]entity.BlockEntity -} - -// Section implements storage of blocks within a fixed 16*16*16 area. -type Section interface { - // GetBlock return block status, offset can be calculate by SectionOffset. - GetBlock(offset uint) BlockStatus - // SetBlock is the reverse operation of GetBlock. - SetBlock(offset uint, s BlockStatus) -} - -func sectionIdx(x, y, z int) uint { - return uint(((y & 15) << 8) | (z << 4) | x) -} - -type BlockStatus uint32 - -type ChunkLoc struct { - X, Z int -} - -// Signs returns a list of signs on the server within viewing range. -func (w *World) Signs() []entity.BlockEntity { - w.chunkLock.RLock() - defer w.chunkLock.RUnlock() - - out := make([]entity.BlockEntity, 0, 4) - for _, c := range w.Chunks { - for _, e := range c.TileEntities { - if e.ID == "minecraft:sign" { - out = append(out, e) - } - } - } - - return out -} - -// GetBlockStatus return the state ID of the block at the given position. -func (w *World) GetBlockStatus(x, y, z int) BlockStatus { - w.chunkLock.RLock() - defer w.chunkLock.RUnlock() - - c := w.Chunks[ChunkLoc{x >> 4, z >> 4}] - if c != nil && y >= 0 { - if sec := c.Sections[y>>4]; sec != nil { - return sec.GetBlock(sectionIdx(x&15, y&15, z&15)) - } - } - return 0 -} - -// UnloadChunk unloads a chunk from the world. -func (w *World) UnloadChunk(loc ChunkLoc) { - w.chunkLock.Lock() - delete(w.Chunks, loc) - w.chunkLock.Unlock() -} - -// UnaryBlockUpdate updates the block at the specified position with a new -// state ID. -func (w *World) UnaryBlockUpdate(pos pk.Position, bStateID BlockStatus) bool { - w.chunkLock.Lock() - defer w.chunkLock.Unlock() - - c := w.Chunks[ChunkLoc{X: pos.X >> 4, Z: pos.Z >> 4}] - if c == nil { - return false - } - sIdx, bIdx := pos.Y>>4, sectionIdx(pos.X&15, pos.Y&15, pos.Z&15) - - if sec := c.Sections[sIdx]; sec == nil { - sec = newSectionWithSize(uint(block.BitsPerBlock)) - sec.SetBlock(bIdx, bStateID) - c.Sections[sIdx] = sec - } else { - tp := ToTilePos(pos.X, pos.Y, pos.Z) - if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID { - delete(c.TileEntities, tp) - } - sec.SetBlock(bIdx, bStateID) - } - return true -} - -// MultiBlockUpdate updates a packed specification of blocks within a single -// section of a chunk. -func (w *World) MultiBlockUpdate(loc ChunkLoc, sectionY int, blocks []pk.VarLong) bool { - w.chunkLock.Lock() - defer w.chunkLock.Unlock() - - c := w.Chunks[loc] - if c == nil { - return false // not loaded - } - - sec := c.Sections[sectionY] - if sec == nil { - sec = newSectionWithSize(uint(block.BitsPerBlock)) - c.Sections[sectionY] = sec - } - - for _, b := range blocks { - var ( - bStateID = BlockStatus(b >> 12) - x, z, y = (b >> 8) & 0xf, (b >> 4) & 0xf, b & 0xf - bIdx = sectionIdx(int(x&15), int(y&15), int(z&15)) - tp = ToTilePos(int(x), int(y), int(z)) - ) - if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID { - delete(c.TileEntities, tp) - } - sec.SetBlock(bIdx, bStateID) - } - - return true -} - -//LoadChunk adds the given chunk to the world. -func (w *World) LoadChunk(x, z int, c *Chunk) { - w.chunkLock.Lock() - w.Chunks[ChunkLoc{X: x, Z: z}] = c - w.chunkLock.Unlock() -} - -func (w *World) TileEntityUpdate(pkt ptypes.TileEntityData) error { - w.chunkLock.Lock() - defer w.chunkLock.Unlock() - c := w.Chunks[ChunkLoc{X: pkt.Pos.X >> 4, Z: pkt.Pos.Z >> 4}] - if c == nil { - return nil - } - - switch pkt.Action { - case 9: // Sign update - c.TileEntities[ToTilePos(pkt.Pos.X, pkt.Pos.Y, pkt.Pos.Z)] = pkt.Data - } - return nil -} diff --git a/bot/world/world_entity.go b/bot/world/world_entity.go deleted file mode 100644 index 7fdf7f7..0000000 --- a/bot/world/world_entity.go +++ /dev/null @@ -1,165 +0,0 @@ -package world - -import ( - "fmt" - - "github.com/Tnze/go-mc/bot/world/entity" - e "github.com/Tnze/go-mc/data/entity" - pk "github.com/Tnze/go-mc/net/packet" - "github.com/Tnze/go-mc/net/ptypes" - "github.com/google/uuid" -) - -// PlayerEntities returns a list of players on the server within viewing range. -func (w *World) PlayerEntities() []entity.Entity { - w.entityLock.RLock() - defer w.entityLock.RUnlock() - out := make([]entity.Entity, 0, 12) - for _, ent := range w.Entities { - if ent.Base.ID == e.Player.ID { - out = append(out, *ent) - } - } - return out -} - -// OnSpawnEntity should be called when a SpawnEntity packet -// is received. -func (w *World) OnSpawnEntity(pkt ptypes.SpawnEntity) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - base, ok := e.ByID[e.ID(pkt.Type)] - if !ok { - return fmt.Errorf("unknown entity ID %v", pkt.Type) - } - - w.Entities[int32(pkt.ID)] = &entity.Entity{ - ID: int32(pkt.ID), - Base: base, - Data: int32(pkt.Data), - UUID: uuid.UUID(pkt.UUID), - X: float64(pkt.X), - Y: float64(pkt.Y), - Z: float64(pkt.Z), - Pitch: int8(pkt.Pitch), - Yaw: int8(pkt.Yaw), - VelX: int16(pkt.VelX), - VelY: int16(pkt.VelY), - VelZ: int16(pkt.VelZ), - } - - return nil -} - -// OnSpawnLivingEntity should be called when a SpawnLivingEntity packet -// is received. -func (w *World) OnSpawnLivingEntity(pkt ptypes.SpawnLivingEntity) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - base, ok := e.ByID[e.ID(pkt.Type)] - if !ok { - return fmt.Errorf("unknown entity ID %v", pkt.Type) - } - - w.Entities[int32(pkt.ID)] = &entity.Entity{ - ID: int32(pkt.ID), - Base: base, - UUID: uuid.UUID(pkt.UUID), - X: float64(pkt.X), - Y: float64(pkt.Y), - Z: float64(pkt.Z), - Pitch: int8(pkt.Pitch), - Yaw: int8(pkt.Yaw), - VelX: int16(pkt.VelX), - VelY: int16(pkt.VelY), - VelZ: int16(pkt.VelZ), - HeadPitch: int8(pkt.HeadPitch), - } - return nil -} - -// OnSpawnPlayer should be called when a SpawnPlayer packet -// is received. -func (w *World) OnSpawnPlayer(pkt ptypes.SpawnPlayer) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - w.Entities[int32(pkt.ID)] = &entity.Entity{ - ID: int32(pkt.ID), - Base: &e.Player, - UUID: uuid.UUID(pkt.UUID), - X: float64(pkt.X), - Y: float64(pkt.Y), - Z: float64(pkt.Z), - Pitch: int8(pkt.Pitch), - Yaw: int8(pkt.Yaw), - } - return nil -} - -// OnEntityPosUpdate should be called when an EntityPosition packet -// is received. -func (w *World) OnEntityPosUpdate(pkt ptypes.EntityPosition) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - ent, ok := w.Entities[int32(pkt.ID)] - if !ok { - return fmt.Errorf("cannot handle position update for unknown entity %d", pkt.ID) - } - - ent.X += float64(pkt.X) / (128 * 32) - ent.Y += float64(pkt.Y) / (128 * 32) - ent.Z += float64(pkt.Z) / (128 * 32) - ent.OnGround = bool(pkt.OnGround) - return nil -} - -// OnEntityPosLookUpdate should be called when an EntityPositionLook packet -// is received. -func (w *World) OnEntityPosLookUpdate(pkt ptypes.EntityPositionLook) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - ent, ok := w.Entities[int32(pkt.ID)] - if !ok { - return fmt.Errorf("cannot handle position look update for unknown entity %d", pkt.ID) - } - - ent.X += float64(pkt.X) / (128 * 32) - ent.Y += float64(pkt.Y) / (128 * 32) - ent.Z += float64(pkt.Z) / (128 * 32) - ent.OnGround = bool(pkt.OnGround) - ent.Pitch, ent.Yaw = int8(pkt.Pitch), int8(pkt.Yaw) - return nil -} - -// OnEntityLookUpdate should be called when an EntityRotation packet -// is received. -func (w *World) OnEntityLookUpdate(pkt ptypes.EntityRotation) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - ent, ok := w.Entities[int32(pkt.ID)] - if !ok { - return fmt.Errorf("cannot handle look update for unknown entity %d", pkt.ID) - } - - ent.Pitch, ent.Yaw = int8(pkt.Pitch), int8(pkt.Yaw) - ent.OnGround = bool(pkt.OnGround) - return nil -} - -// OnEntityDestroy should be called when a DestroyEntities packet -// is received. -func (w *World) OnEntityDestroy(eIDs []pk.VarInt) error { - w.entityLock.Lock() - defer w.entityLock.Unlock() - - for _, eID := range eIDs { - delete(w.Entities, int32(eID)) - } - return nil -} diff --git a/cmd/daze/daze.go b/cmd/daze/daze.go index 970747c..b998b42 100644 --- a/cmd/daze/daze.go +++ b/cmd/daze/daze.go @@ -1,49 +1,61 @@ package main import ( - "bytes" + "errors" "flag" "log" "github.com/google/uuid" "github.com/Tnze/go-mc/bot" + "github.com/Tnze/go-mc/bot/basic" "github.com/Tnze/go-mc/chat" _ "github.com/Tnze/go-mc/data/lang/zh-cn" - pk "github.com/Tnze/go-mc/net/packet" ) var address = flag.String("address", "127.0.0.1", "The server address") -var c *bot.Client +var client *bot.Client +var player *basic.Player func main() { flag.Parse() - c = bot.NewClient() + client = bot.NewClient() + player = basic.NewPlayer(client, basic.DefaultSettings) + basic.EventsListener{ + GameStart: onGameStart, + ChatMsg: onChatMsg, + Disconnect: onDisconnect, + Death: onDeath, + }.Attach(client) //Login - err := c.JoinServer(*address) + err := client.JoinServer(*address) if err != nil { log.Fatal(err) } log.Println("Login success") //Register event handlers - c.Events.GameStart = onGameStart - c.Events.ChatMsg = onChatMsg - c.Events.Disconnect = onDisconnect - c.Events.PluginMessage = onPluginMessage //JoinGame - err = c.HandleGame() - if err != nil { - log.Fatal(err) + for { + if err = client.HandleGame(); err != nil { + var interErr *bot.PacketHandlerError + if errors.As(err, &interErr) { + log.Print("Internal bugs: ", interErr) + } else { + log.Fatal(err) + } + } else { + break + } } } func onDeath() error { log.Println("Died and Respawned") // If we exclude Respawn(...) then the player won't press the "Respawn" button upon death - return c.Respawn() + return player.Respawn() } func onGameStart() error { @@ -56,22 +68,7 @@ func onChatMsg(c chat.Message, pos byte, uuid uuid.UUID) error { return nil } -func onDisconnect(c chat.Message) error { - log.Println("Disconnect:", c) - return nil -} - -func onPluginMessage(channel string, data []byte) error { - switch channel { - case "minecraft:brand": - var brand pk.String - if _, err := brand.ReadFrom(bytes.NewReader(data)); err != nil { - return err - } - log.Println("Server brand is:", brand) - - default: - log.Println("PluginMessage", channel, data) - } +func onDisconnect(reason chat.Message) error { + log.Println("Disconnect:", reason) return nil } diff --git a/cmd/mcping/mcping.go b/cmd/mcping/mcping.go index 016bb3f..5ff98ad 100644 --- a/cmd/mcping/mcping.go +++ b/cmd/mcping/mcping.go @@ -5,12 +5,10 @@ import ( "encoding/json" "fmt" "os" - "strconv" "strings" "github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/chat" - _ "github.com/Tnze/go-mc/data/lang/en-us" "github.com/google/uuid" ) @@ -32,11 +30,9 @@ type status struct { } func main() { - addr, port := getAddr() - - fmt.Printf("MCPING (%s:%d):\n", addr, port) - - resp, delay, err := bot.PingAndList(addr, port) + addr := getAddr() + fmt.Printf("MCPING (%s):\n", addr) + resp, delay, err := bot.PingAndList(addr) if err != nil { fmt.Printf("ping and list server fail: %v", err) os.Exit(1) @@ -53,28 +49,14 @@ func main() { fmt.Println("Delay:", delay) } -func getAddr() (string, int) { +func getAddr() string { const usage = "Usage: mcping [:port]" if len(os.Args) < 2 { fmt.Println("no host name.", usage) os.Exit(1) } - addr := strings.Split(os.Args[1], ":") - var port int - switch len(addr) { - case 1: - port = 25565 - case 2: - var err error - port, err = strconv.Atoi(addr[1]) - if err != nil { - fmt.Println(err, usage) - os.Exit(1) - } - } - - return addr[0], port + return os.Args[1] } func (s status) String() string { diff --git a/data/packetid/gen_packetIDs.go b/data/packetid/gen_packetIDs.go index f8c47cd..60efa13 100644 --- a/data/packetid/gen_packetIDs.go +++ b/data/packetid/gen_packetIDs.go @@ -19,27 +19,35 @@ const ( package packetid -// Valid PktID values. +// Login state const ( - // Clientbound packets for connections in the login state. + // Clientbound {{range $Name, $ID := .Login.Clientbound}} {{$Name}} = {{$ID}} {{end}} - // Serverbound packets for connections in the login state + // Serverbound {{range $Name, $ID := .Login.Serverbound}} {{$Name}} = {{$ID}} {{end}} - // Clientbound packets for connections in the play state. -{{range $Name, $ID := .Play.Clientbound}} {{$Name}} = {{$ID}} -{{end}} - // Serverbound packets for connections in the play state. -{{range $Name, $ID := .Play.Serverbound}} {{$Name}} = {{$ID}} -{{end}} - // Clientbound packets used to respond to ping/status requests. +) + +// Ping state +const ( + // Clientbound {{range $Name, $ID := .Status.Clientbound}} {{$Name}} = {{$ID}} {{end}} - // Serverbound packets used to ping or read server status. + // Serverbound {{range $Name, $ID := .Status.Serverbound}} {{$Name}} = {{$ID}} {{end}} ) + +// Play state +const ( + // Clientbound +{{range $Name, $ID := .Play.Clientbound}} {{$Name}} = {{$ID}} +{{end}} + // Serverbound +{{range $Name, $ID := .Play.Serverbound}} {{$Name}} = {{$ID}} +{{end}} +) ` ) diff --git a/go.mod b/go.mod index dac3ddc..b130f4a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 + github.com/fatih/set v0.2.1 // indirect github.com/google/uuid v1.1.1 github.com/iancoleman/strcase v0.1.3 github.com/mattn/go-colorable v0.1.8 diff --git a/go.sum b/go.sum index eaf6ffb..6af4fad 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 h1:p4g4uok3+r6Tg6fxXEQUAcMAX/WdK6WhkQW9s0jaT7k= github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc= +github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw= diff --git a/nbt/nbt.go b/nbt/nbt.go index 53cf9f7..15682f5 100644 --- a/nbt/nbt.go +++ b/nbt/nbt.go @@ -30,7 +30,7 @@ func IsArrayTag(ty byte) bool { } type DecoderReader = interface { - io.ByteScanner + io.ByteReader io.Reader } type Decoder struct { @@ -42,7 +42,7 @@ func NewDecoder(r io.Reader) *Decoder { if br, ok := r.(DecoderReader); ok { d.r = br } else { - d.r = bufio.NewReader(r) + d.r = bufio.NewReaderSize(r, 0) } return d } diff --git a/net/conn.go b/net/conn.go index 88e3143..bf26862 100644 --- a/net/conn.go +++ b/net/conn.go @@ -36,10 +36,7 @@ func (l Listener) Accept() (Conn, error) { //Conn is a minecraft Connection type Conn struct { Socket net.Conn - Reader interface { - io.ByteReader - io.Reader - } + io.Reader io.Writer threshold int @@ -50,7 +47,7 @@ func DialMC(addr string) (*Conn, error) { conn, err := net.Dial("tcp", addr) return &Conn{ Socket: conn, - Reader: bufio.NewReader(conn), + Reader: conn, Writer: conn, }, err } @@ -60,7 +57,7 @@ func DialMCTimeout(addr string, timeout time.Duration) (*Conn, error) { conn, err := net.DialTimeout("tcp", addr, timeout) return &Conn{ Socket: conn, - Reader: bufio.NewReader(conn), + Reader: conn, Writer: conn, }, err } @@ -70,7 +67,7 @@ func DialMCTimeout(addr string, timeout time.Duration) (*Conn, error) { func WrapConn(conn net.Conn) *Conn { return &Conn{ Socket: conn, - Reader: bufio.NewReader(conn), + Reader: conn, Writer: conn, } } @@ -91,10 +88,10 @@ func (c *Conn) WritePacket(p pk.Packet) error { // SetCipher load the decode/encode stream to this Conn func (c *Conn) SetCipher(ecoStream, decoStream cipher.Stream) { //加密连接 - c.Reader = bufio.NewReader(cipher.StreamReader{ //Set receiver for AES + c.Reader = cipher.StreamReader{ //Set receiver for AES S: decoStream, R: c.Socket, - }) + } c.Writer = cipher.StreamWriter{ S: ecoStream, W: c.Socket, diff --git a/net/packet/joingame_test.bin b/net/packet/joingame_test.bin new file mode 100644 index 0000000..75f2926 Binary files /dev/null and b/net/packet/joingame_test.bin differ diff --git a/net/packet/packet.go b/net/packet/packet.go index c33f3ac..8d37a24 100644 --- a/net/packet/packet.go +++ b/net/packet/packet.go @@ -3,8 +3,10 @@ package packet import ( "bytes" "compress/zlib" + "encoding/hex" "fmt" "io" + "os" ) // Packet define a net data package @@ -25,8 +27,9 @@ func Marshal(id int32, fields ...FieldEncoder) (pk Packet) { //Scan decode the packet and fill data into fields func (p Packet) Scan(fields ...FieldDecoder) error { r := bytes.NewReader(p.Data) + rr := io.TeeReader(r, hex.Dumper(os.Stdout)) for _, v := range fields { - _, err := v.ReadFrom(r) + _, err := v.ReadFrom(rr) if err != nil { return err } @@ -82,25 +85,25 @@ func (p *Packet) UnPack(r io.Reader, threshold int) error { if length < 1 { return fmt.Errorf("packet length too short") } - - buf := bytes.NewBuffer(p.Data[:0]) - if _, err := io.CopyN(buf, r, int64(length)); err != nil { + buf := make([]byte, length) + if _, err := io.ReadFull(r, buf); err != nil { return fmt.Errorf("read content of packet fail: %w", err) } + buffer := bytes.NewBuffer(buf) //解压数据 if threshold > 0 { - if err := unCompress(buf); err != nil { + if err := unCompress(buffer); err != nil { return err } } var packetID VarInt - if _, err := packetID.ReadFrom(buf); err != nil { + if _, err := packetID.ReadFrom(buffer); err != nil { return fmt.Errorf("read packet id fail: %v", err) } p.ID = int32(packetID) - p.Data = buf.Bytes() + p.Data = buffer.Bytes() return nil } diff --git a/net/packet/packet_test.go b/net/packet/packet_test.go index 6c277ed..87b184f 100644 --- a/net/packet/packet_test.go +++ b/net/packet/packet_test.go @@ -1,11 +1,14 @@ -package packet +package packet_test import ( "bytes" + _ "embed" "testing" + + pk "github.com/Tnze/go-mc/net/packet" ) -var VarInts = []VarInt{0, 1, 2, 127, 128, 255, 2147483647, -1, -2147483648} +var VarInts = []pk.VarInt{0, 1, 2, 127, 128, 255, 2147483647, -1, -2147483648} var PackedVarInts = [][]byte{ {0x00}, @@ -35,7 +38,7 @@ func TestPackVarInt(t *testing.T) { } func TestUnpackVarInt(t *testing.T) { for i, v := range PackedVarInts { - var vi VarInt + var vi pk.VarInt if _, err := vi.ReadFrom(bytes.NewReader(v)); err != nil { t.Errorf("unpack \"% x\" error: %v", v, err) } @@ -46,7 +49,7 @@ func TestUnpackVarInt(t *testing.T) { } func TestUnpackVarInt_TooLongData(t *testing.T) { - var vi VarInt + var vi pk.VarInt var data = []byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01} if _, err := vi.ReadFrom(bytes.NewReader(data)); err != nil { t.Logf("unpack \"% x\" error: %v", data, err) @@ -55,7 +58,7 @@ func TestUnpackVarInt_TooLongData(t *testing.T) { } } -var VarLongs = []VarLong{0, 1, 2, 127, 128, 255, 2147483647, 9223372036854775807, -1, -2147483648, -9223372036854775808} +var VarLongs = []pk.VarLong{0, 1, 2, 127, 128, 255, 2147483647, 9223372036854775807, -1, -2147483648, -9223372036854775808} var PackedVarLongs = [][]byte{ {0x00}, @@ -81,7 +84,7 @@ func TestPackVarLong(t *testing.T) { } func TestUnpackVarLong(t *testing.T) { for i, v := range PackedVarLongs { - var vi VarLong + var vi pk.VarLong if _, err := vi.ReadFrom(bytes.NewReader(v)); err != nil { t.Errorf("unpack \"% x\" error: %v", v, err) } @@ -90,3 +93,46 @@ func TestUnpackVarLong(t *testing.T) { } } } + +//go:embed joingame_test.bin +var joingame []byte + +func TestJoinGamePacket(t *testing.T) { + p := pk.Packet{ID: 0x24, Data: joingame} + var ( + EID pk.Int + Hardcore pk.Boolean + Gamemode pk.UnsignedByte + PreGamemode pk.Byte + WorldCount pk.VarInt + WorldNames = pk.Ary{Len: &WorldCount, Ary: &[]pk.String{}} + DimensionCodec struct { + DimensionType interface{} `nbt:"minecraft:dimension_type"` + WorldgenBiome interface{} `nbt:"minecraft:worldgen/biome"` + } + Dimension interface{} + WorldName pk.Identifier + HashedSeed pk.Long + MaxPlayers pk.VarInt + ViewDistance pk.VarInt + RDI, ERS, IsDebug, IsFlat pk.Boolean + ) + err := p.Scan( + &EID, + &Hardcore, + &Gamemode, + &PreGamemode, + &WorldCount, + WorldNames, + &pk.NBT{V: &DimensionCodec}, + &pk.NBT{V: &Dimension}, + &WorldName, + &HashedSeed, + &MaxPlayers, + &ViewDistance, + &RDI, &ERS, &IsDebug, &IsFlat, + ) + if err != nil { + t.Error(err) + } +} diff --git a/net/packet/util.go b/net/packet/util.go index 216744a..369fafb 100644 --- a/net/packet/util.go +++ b/net/packet/util.go @@ -6,13 +6,13 @@ import ( ) type Ary struct { - Len Field // One of VarInt, VarLong, Int or Long - Ary interface{} // FieldEncoder, FieldDecoder or both (Field) + Len Field // Pointer of VarInt, VarLong, Int or Long + Ary interface{} // Slice of FieldEncoder, FieldDecoder or both (Field) } func (a Ary) WriteTo(r io.Writer) (n int64, err error) { length := int(reflect.ValueOf(a.Len).Int()) - array := reflect.ValueOf(a.Ary).Elem() + array := reflect.ValueOf(a.Ary) for i := 0; i < length; i++ { elem := array.Index(i) nn, err := elem.Interface().(FieldEncoder).WriteTo(r) @@ -25,11 +25,15 @@ func (a Ary) WriteTo(r io.Writer) (n int64, err error) { } func (a Ary) ReadFrom(r io.Reader) (n int64, err error) { - length := int(reflect.ValueOf(a.Len).Int()) + length := int(reflect.ValueOf(a.Len).Elem().Int()) array := reflect.ValueOf(a.Ary).Elem() + if array.Cap() < length { + array = reflect.MakeSlice(array.Type(), length, length) + a.Ary = array.Interface() + } for i := 0; i < length; i++ { elem := array.Index(i) - nn, err := elem.Interface().(FieldDecoder).ReadFrom(r) + nn, err := elem.Addr().Interface().(FieldDecoder).ReadFrom(r) n += nn if err != nil { return n, err diff --git a/net/ptypes/chunk.go b/net/ptypes/chunk.go deleted file mode 100644 index ae77f57..0000000 --- a/net/ptypes/chunk.go +++ /dev/null @@ -1,82 +0,0 @@ -// Package ptypes implements encoding and decoding for high-level packets. -package ptypes - -import ( - "io" - "math" - - "github.com/Tnze/go-mc/bot/world/entity" - "github.com/Tnze/go-mc/nbt" - pk "github.com/Tnze/go-mc/net/packet" -) - -// ChunkData is a client-bound packet which describes a chunk. -type ChunkData struct { - X, Z pk.Int - FullChunk pk.Boolean - PrimaryBitMask pk.VarInt - Heightmaps struct{} - Biomes biomesData - Data pk.ByteArray - BlockEntities blockEntities -} - -func (p *ChunkData) ReadFrom(r io.Reader) (int64, error) { - return pk.Tuple{ - &p.X, - &p.Z, - &p.FullChunk, - &p.PrimaryBitMask, - &pk.NBT{V: &p.Heightmaps}, - pk.Opt{Has: &p.FullChunk, Field: &p.Biomes}, - &p.Data, - &p.BlockEntities, - }.ReadFrom(r) -} - -type biomesData struct { - data []pk.VarInt -} - -func (b *biomesData) ReadFrom(r io.Reader) (int64, error) { - var n pk.VarInt // Number of Biomes Data - return pk.Tuple{ - &n, pk.Ary{Len: &n, Ary: []pk.VarInt{}}, - }.ReadFrom(r) -} - -type blockEntities []entity.BlockEntity - -// Decode implement net.packet.FieldDecoder -func (b *blockEntities) ReadFrom(r io.Reader) (n int64, err error) { - var sz pk.VarInt // Number of BlockEntities - if nn, err := sz.ReadFrom(r); err != nil { - return nn, err - } else { - n += nn - } - *b = make(blockEntities, sz) - lr := &io.LimitedReader{R: r, N: math.MaxInt64} - d := nbt.NewDecoder(lr) - for i := 0; i < int(sz); i++ { - if err := d.Decode(&(*b)[i]); err != nil { - return math.MaxInt64 - lr.N, err - } - } - return math.MaxInt64 - lr.N, nil -} - -// TileEntityData describes a change to a tile entity. -type TileEntityData struct { - Pos pk.Position - Action pk.UnsignedByte - Data entity.BlockEntity -} - -func (p *TileEntityData) ReadFrom(r io.Reader) (int64, error) { - return pk.Tuple{ - &p.Pos, - &p.Action, - &pk.NBT{V: &p.Data}, - }.ReadFrom(r) -} diff --git a/net/ptypes/entities.go b/net/ptypes/entities.go deleted file mode 100644 index 9d81b6a..0000000 --- a/net/ptypes/entities.go +++ /dev/null @@ -1,95 +0,0 @@ -package ptypes - -import pk "github.com/Tnze/go-mc/net/packet" - -// SpawnEntity is a client-bound packet used to spawn a non-mob entity. -type SpawnEntity struct { - ID pk.VarInt - UUID pk.UUID - Type pk.VarInt - X, Y, Z pk.Double - Pitch, Yaw pk.Angle - Data pk.Int - VelX, VelY, VelZ pk.Short -} - -func (p *SpawnEntity) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.UUID, &p.Type, - &p.X, &p.Y, &p.Z, &p.Pitch, &p.Yaw, - &p.Data, &p.VelX, &p.VelY, &p.VelZ) -} - -// SpawnPlayer is a client-bound packet used to describe a player entering -// visible range. -type SpawnPlayer struct { - ID pk.VarInt - UUID pk.UUID - X, Y, Z pk.Double - Yaw, Pitch pk.Angle -} - -func (p *SpawnPlayer) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.UUID, &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch) -} - -// SpawnLivingEntity is a client-bound packet used to spawn a mob. -type SpawnLivingEntity struct { - ID pk.VarInt - UUID pk.UUID - Type pk.VarInt - X, Y, Z pk.Double - Yaw, Pitch pk.Angle - HeadPitch pk.Angle - VelX, VelY, VelZ pk.Short -} - -func (p *SpawnLivingEntity) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.UUID, &p.Type, - &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, - &p.HeadPitch, &p.VelX, &p.VelY, &p.VelZ) -} - -// EntityAnimationClientbound updates the animation state of an entity. -type EntityAnimationClientbound struct { - ID pk.VarInt - Animation pk.UnsignedByte -} - -func (p *EntityAnimationClientbound) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.Animation) -} - -// EntityPosition is a clientbound packet used to update an entities position. -type EntityPosition struct { - ID pk.VarInt - X, Y, Z pk.Short // Deltas - OnGround pk.Boolean -} - -func (p *EntityPosition) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.X, &p.Y, &p.Z, &p.OnGround) -} - -// EntityPosition is a clientbound packet used to update an entities position -// and its rotation. -type EntityPositionLook struct { - ID pk.VarInt - X, Y, Z pk.Short // Deltas - Yaw, Pitch pk.Angle - OnGround pk.Boolean -} - -func (p *EntityPositionLook) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, &p.OnGround) -} - -// EntityRotation is a clientbound packet used to update an entities rotation. -type EntityRotation struct { - ID pk.VarInt - Yaw, Pitch pk.Angle - OnGround pk.Boolean -} - -func (p *EntityRotation) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.ID, &p.Yaw, &p.Pitch, &p.OnGround) -} diff --git a/net/ptypes/inventory.go b/net/ptypes/inventory.go deleted file mode 100644 index 3cb44cb..0000000 --- a/net/ptypes/inventory.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package ptypes implements encoding and decoding for high-level packets. -package ptypes - -import ( - "errors" - "io" - - "github.com/Tnze/go-mc/bot/world/entity" - "github.com/Tnze/go-mc/chat" - "github.com/Tnze/go-mc/data/packetid" - "github.com/Tnze/go-mc/nbt" - pk "github.com/Tnze/go-mc/net/packet" -) - -// SetSlot is a clientbound packet which configures an inventory slot. -// A window ID of -1 represents the cursor, and a window ID of 0 represents -// the players inventory. -type SetSlot struct { - WindowID pk.Byte - Slot pk.Short - SlotData entity.Slot -} - -func (p *SetSlot) Decode(pkt pk.Packet) error { - if err := pkt.Scan(&p.WindowID, &p.Slot, &p.SlotData); err != nil && !errors.Is(err, nbt.ErrEND) { - return err - } - return nil -} - -// WindowItems is a clientbound packet describing the contents of multiple -// inventory slots in a window/inventory. -type WindowItems struct { - WindowID pk.Byte - Slots []entity.Slot -} - -func (p *WindowItems) ReadFrom(r io.Reader) (int64, error) { - var count pk.Short - return pk.Tuple{ - &p.WindowID, - &count, - &pk.Ary{ - Len: &count, - Ary: []entity.Slot{}, - }, - }.ReadFrom(r) -} - -// OpenWindow is a clientbound packet which opens an inventory. -type OpenWindow struct { - WindowID pk.VarInt - WindowType pk.VarInt - Title chat.Message -} - -func (p *OpenWindow) Decode(pkt pk.Packet) error { - if err := pkt.Scan(&p.WindowID, &p.WindowType, &p.Title); err != nil && !errors.Is(err, nbt.ErrEND) { - return err - } - return nil -} - -type ConfirmTransaction struct { - WindowID pk.Byte - ActionID pk.Short - Accepted pk.Boolean -} - -func (p *ConfirmTransaction) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.WindowID, &p.ActionID, &p.Accepted) -} - -func (p ConfirmTransaction) Encode() pk.Packet { - return pk.Marshal( - packetid.TransactionServerbound, - p.WindowID, - p.ActionID, - p.Accepted, - ) -} diff --git a/net/ptypes/misc.go b/net/ptypes/misc.go deleted file mode 100644 index 591367c..0000000 --- a/net/ptypes/misc.go +++ /dev/null @@ -1,92 +0,0 @@ -package ptypes - -import ( - "io" - - "github.com/Tnze/go-mc/chat" - "github.com/Tnze/go-mc/data/packetid" - pk "github.com/Tnze/go-mc/net/packet" -) - -// SoundEffect is a client-bound packet used to play a specific sound ID -// on the client. -type SoundEffect struct { - Sound pk.VarInt - Category pk.VarInt - X, Y, Z pk.Int - Volume, Pitch pk.Float -} - -func (p *SoundEffect) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.Sound, &p.Category, &p.X, &p.Y, &p.Z, &p.Volume, &p.Pitch) -} - -// NamedSoundEffect is a client-bound packet used to play a sound with the -// specified name on the client. -type NamedSoundEffect struct { - Sound pk.String - Category pk.VarInt - X, Y, Z pk.Int - Volume, Pitch pk.Float -} - -func (p *NamedSoundEffect) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.Sound, &p.Category, &p.X, &p.Y, &p.Z, &p.Volume, &p.Pitch) -} - -// ChatMessageClientbound represents a chat message forwarded by the server. -type ChatMessageClientbound struct { - S chat.Message - Pos pk.Byte - Sender pk.UUID -} - -func (p *ChatMessageClientbound) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.S, &p.Pos, &p.Sender) -} - -// UpdateHealth encodes player health/food information from the server. -type UpdateHealth struct { - Health pk.Float - Food pk.VarInt - FoodSaturation pk.Float -} - -func (p *UpdateHealth) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.Health, &p.Food, &p.FoodSaturation) -} - -// PluginData encodes the custom data encoded in a plugin message. -type PluginData []byte - -func (p PluginData) WriteTo(w io.Writer) (int64, error) { - n, err := w.Write(p) - return int64(n), err -} - -func (p *PluginData) ReadFrom(r io.Reader) (int64, error) { - d, err := io.ReadAll(r) - if err != nil { - return int64(len(d)), err - } - *p = d - return int64(len(d)), nil -} - -// PluginMessage represents a packet with a customized payload. -type PluginMessage struct { - Channel pk.Identifier - Data PluginData -} - -func (p *PluginMessage) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.Channel, &p.Data) -} - -func (p *PluginMessage) Encode() pk.Packet { - return pk.Marshal( - packetid.CustomPayloadServerbound, - p.Channel, - p.Data, - ) -} diff --git a/net/ptypes/motion.go b/net/ptypes/motion.go deleted file mode 100644 index b2e346d..0000000 --- a/net/ptypes/motion.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package ptypes implements encoding and decoding for high-level packets. -package ptypes - -import ( - "github.com/Tnze/go-mc/data/packetid" - pk "github.com/Tnze/go-mc/net/packet" -) - -// PositionAndLookClientbound describes the location and orientation of -// the player. -type PositionAndLookClientbound struct { - X, Y, Z pk.Double - Yaw, Pitch pk.Float - Flags pk.Byte - TeleportID pk.VarInt -} - -func (p *PositionAndLookClientbound) RelativeX() bool { - return p.Flags&0x01 != 0 -} -func (p *PositionAndLookClientbound) RelativeY() bool { - return p.Flags&0x02 != 0 -} -func (p *PositionAndLookClientbound) RelativeZ() bool { - return p.Flags&0x04 != 0 -} -func (p *PositionAndLookClientbound) RelativeYaw() bool { - return p.Flags&0x08 != 0 -} -func (p *PositionAndLookClientbound) RelativePitch() bool { - return p.Flags&0x10 != 0 -} - -func (p *PositionAndLookClientbound) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, &p.Flags, &p.TeleportID) -} - -// PositionAndLookServerbound describes the location and orientation of -// the player. -type PositionAndLookServerbound struct { - X, Y, Z pk.Double - Yaw, Pitch pk.Float - OnGround pk.Boolean -} - -func (p PositionAndLookServerbound) Encode() pk.Packet { - return pk.Marshal( - packetid.PositionLook, - p.X, p.Y, p.Z, - p.Yaw, p.Pitch, - p.OnGround, - ) -} - -// Position describes the position of the player. -type Position struct { - X, Y, Z pk.Double - OnGround pk.Boolean -} - -func (p Position) Encode() pk.Packet { - return pk.Marshal( - packetid.PositionServerbound, - p.X, p.Y, p.Z, - p.OnGround, - ) -} - -// Look describes the rotation of the player. -type Look struct { - Yaw, Pitch pk.Float - OnGround pk.Boolean -} - -func (p Look) Encode() pk.Packet { - return pk.Marshal( - packetid.Look, - p.Yaw, p.Pitch, - p.OnGround, - ) -} diff --git a/net/ptypes/world.go b/net/ptypes/world.go deleted file mode 100644 index 477b102..0000000 --- a/net/ptypes/world.go +++ /dev/null @@ -1,32 +0,0 @@ -package ptypes - -import ( - pk "github.com/Tnze/go-mc/net/packet" -) - -// JoinGame encodes global/world information from the server. -type JoinGame struct { - PlayerEntity pk.Int - Hardcore pk.Boolean - Gamemode pk.UnsignedByte - PrevGamemode pk.UnsignedByte - WorldCount pk.VarInt - WorldNames pk.Identifier - //DimensionCodec pk.NBT - Dimension pk.Int - WorldName pk.Identifier - HashedSeed pk.Long - maxPlayers pk.VarInt // Now ignored - ViewDistance pk.VarInt - RDI pk.Boolean // Reduced Debug Info - ERS pk.Boolean // Enable respawn screen - IsDebug pk.Boolean - IsFlat pk.Boolean -} - -func (p *JoinGame) Decode(pkt pk.Packet) error { - return pkt.Scan(&p.PlayerEntity, &p.Hardcore, &p.Gamemode, &p.PrevGamemode, - &p.WorldCount, &p.WorldNames, &p.Dimension, - &p.WorldName, &p.HashedSeed, &p.maxPlayers, &p.ViewDistance, - &p.RDI, &p.ERS, &p.IsDebug, &p.IsFlat) -}