diff --git a/bot/basic/basic.go b/bot/basic/basic.go index 2378732..94a1364 100644 --- a/bot/basic/basic.go +++ b/bot/basic/basic.go @@ -7,14 +7,13 @@ // There is 4 kinds of clientbound packet is handled by this package. // - LoginPacket, for cache player info. The player info will be stored in [Player.PlayerInfo]. // - KeepAlivePacket, for avoid the client to be kicked by the server. -// - PlayerPosition, is only received when server teleporting the player. And the confirm packet is automatically sent. +// - PlayerPosition, is only received when server teleporting the player. // - Respawn, for updating player info, which may change when player respawned. // // # [EventsListener] // // Handles some basic event you probably need. // - GameStart -// - ChatMsg // - Disconnect // - HealthChange // - Death @@ -32,21 +31,22 @@ type Player struct { PlayerInfo WorldInfo - isSpawn bool } +// NewPlayer create a new Player manager. func NewPlayer(c *bot.Client, settings Settings, events EventsListener) *Player { p := &Player{c: c, Settings: settings} c.Events.AddListener( bot.PacketHandler{Priority: 0, ID: packetid.ClientboundLogin, F: p.handleLoginPacket}, bot.PacketHandler{Priority: 0, ID: packetid.ClientboundKeepAlive, F: p.handleKeepAlivePacket}, - bot.PacketHandler{Priority: 0, ID: packetid.ClientboundPlayerPosition, F: p.handlePlayerPosition}, bot.PacketHandler{Priority: 0, ID: packetid.ClientboundRespawn, F: p.handleRespawnPacket}, ) events.attach(p) return p } +// Respawn is used to send a respawn packet to the server. +// Typically, you should call this method when the player is dead (in the [Death] event handler). func (p *Player) Respawn() error { const PerformRespawn = 0 @@ -61,6 +61,19 @@ func (p *Player) Respawn() error { return nil } +// AcceptTeleportation is used to send a teleport confirmation packet to the server. +// Typically, you should call this method when received a ClientboundPlayerPosition packet (in the [Teleported] event handler). +func (p *Player) AcceptTeleportation(teleportID pk.VarInt) error { + err := p.c.Conn.WritePacket(pk.Marshal( + packetid.ServerboundAcceptTeleportation, + teleportID, + )) + if err != nil { + return Error{err} + } + return nil +} + type Error struct { Err error } diff --git a/bot/basic/events.go b/bot/basic/events.go index 37b7b74..a1b3b03 100644 --- a/bot/basic/events.go +++ b/bot/basic/events.go @@ -7,11 +7,49 @@ import ( pk "github.com/Tnze/go-mc/net/packet" ) +// EventsListener is a collection of event handlers. +// Fill the fields with your handler functions and pass it to [NewPlayer] to create the Player manager. +// For the event you don't want to handle, just leave it nil. type EventsListener struct { - GameStart func() error - Disconnect func(reason chat.Message) error - HealthChange func(health float32) error - Death func() error + // GameStart event is called when the login process is completed and the player is ready to play. + // + // If you want to do some action when the bot joined the server like sending a chat message, + // this event is the right place to do it. + GameStart func() error + + // Disconnect event is called before the server disconnects your client. + // When the server willfully disconnects the client, it will send a ClientboundDisconnect packet and tell you why. + // On vanilla client, the reason is displayed in the disconnect screen. + // + // This information may be very useful for debugging, and generally you should record it into the log. + // + // If the connection is disconnected due to network reasons or the client's initiative, + // this event will not be triggered. + Disconnect func(reason chat.Message) error + + // HealthChange event is called when the player's health or food changed. + HealthChange func(health float32, foodLevel int32, foodSaturation float32) error + + // Death event is a special case of HealthChange. + // It will be called after HealthChange handler called (if it isn't nil) + // when the player's health is less than or equal to 0. + // + // Typically, you should call [Player.Respawn] in this handler. + Death func() error + + // Teleported event is called when the server think the player position in the client side is wrong, + // and send a ClientboundPlayerPosition packet to correct the client. + // + // Typically, you need to do two things in this handler: + // - Update the player's position and rotation you tracked to the correct position. + // - Call [Player.AcceptTeleportation] to send a teleport confirmation packet to the server. + // + // Before you confirm the teleportation, the server will not accept any player motion packets. + // + // The position coordinates and rotation are absolute or relative depends on the flags. + // The flag byte is a bitfield, specifies whether each coordinate value is absolute or relative. + // For more information, see https://wiki.vg/Protocol#Synchronize_Player_Position + Teleported func(x, y, z float64, yaw, pitch float32, flags byte, teleportID int32, dismountVehicle bool) error } // attach your event listener to the client. @@ -26,6 +64,9 @@ func (e EventsListener) attach(p *Player) { if e.HealthChange != nil || e.Death != nil { attachUpdateHealth(p.c, e.HealthChange, e.Death) } + if e.Teleported != nil { + attachPlayerPosition(p.c, e.Teleported) + } } func attachJoinGameHandler(c *bot.Client, handler func() error) { @@ -50,20 +91,20 @@ func attachDisconnect(c *bot.Client, handler func(reason chat.Message) error) { }) } -func attachUpdateHealth(c *bot.Client, healthChangeHandler func(health float32) error, deathHandler func() error) { +func attachUpdateHealth(c *bot.Client, healthChangeHandler func(health float32, food int32, saturation float32) error, deathHandler func() error) { c.Events.AddListener(bot.PacketHandler{ Priority: 64, ID: packetid.ClientboundSetHealth, F: func(p pk.Packet) error { var health pk.Float var food pk.VarInt - var foodSaturation pk.Float + var saturation pk.Float - if err := p.Scan(&health, &food, &foodSaturation); err != nil { + if err := p.Scan(&health, &food, &saturation); err != nil { return Error{err} } var healthChangeErr, deathErr error if healthChangeHandler != nil { - healthChangeErr = healthChangeHandler(float32(health)) + healthChangeErr = healthChangeHandler(float32(health), int32(food), float32(saturation)) } if deathHandler != nil && health <= 0 { deathErr = deathHandler() @@ -76,6 +117,25 @@ func attachUpdateHealth(c *bot.Client, healthChangeHandler func(health float32) }) } +func attachPlayerPosition(c *bot.Client, handler func(x, y, z float64, yaw, pitch float32, flag byte, teleportID int32, dismountVehicle bool) error) { + c.Events.AddListener(bot.PacketHandler{ + Priority: 64, ID: packetid.ClientboundPlayerPosition, + F: func(p pk.Packet) error { + var ( + X, Y, Z pk.Double + Yaw, Pitch pk.Float + Flags pk.Byte + TeleportID pk.VarInt + DismountVehicle pk.Boolean + ) + if err := p.Scan(&X, &Y, &Z, &Yaw, &Pitch, &Flags, &TeleportID, &DismountVehicle); err != nil { + return Error{err} + } + return handler(float64(X), float64(Y), float64(Z), float32(Yaw), float32(Pitch), byte(Flags), int32(TeleportID), bool(DismountVehicle)) + }, + }) +} + type updateHealthError struct { healthChangeErr, deathErr error } diff --git a/bot/basic/info.go b/bot/basic/info.go index d6347b7..fe6460a 100644 --- a/bot/basic/info.go +++ b/bot/basic/info.go @@ -18,7 +18,7 @@ type WorldInfo struct { MaxPlayers int32 // Was once used by the client to draw the player list, but now is ignored. ViewDistance int32 // Render distance (2-32). SimulationDistance int32 // The distance that the client will process specific things, such as entities. - ReducedDebugInfo bool // If true, a Notchian client shows reduced information on the debug screen. For servers in development, this should almost always be false. + ReducedDebugInfo bool // If true, a vanilla 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. @@ -31,11 +31,6 @@ type PlayerInfo struct { PrevGamemode int8 // Previous Gamemode } -// ServInfo contains information about the server implementation. -type ServInfo struct { - Brand string -} - func (p *Player) handleLoginPacket(packet pk.Packet) error { err := packet.Scan( (*pk.Int)(&p.EID), diff --git a/bot/basic/keepalive.go b/bot/basic/keepalive.go index c1502a9..040385b 100644 --- a/bot/basic/keepalive.go +++ b/bot/basic/keepalive.go @@ -20,41 +20,3 @@ func (p *Player) handleKeepAlivePacket(packet pk.Packet) error { } return nil } - -func (p *Player) handlePlayerPosition(packet pk.Packet) error { - var ( - X, Y, Z pk.Double - Yaw, Pitch pk.Float - Flags pk.Byte - TeleportID pk.VarInt - DismountVehicle pk.Boolean - ) - if err := packet.Scan(&X, &Y, &Z, &Yaw, &Pitch, &Flags, &TeleportID, &DismountVehicle); err != nil { - return Error{err} - } - - // Teleport Confirm - err := p.c.Conn.WritePacket(pk.Marshal( - packetid.ServerboundAcceptTeleportation, - TeleportID, - )) - if err != nil { - return Error{err} - } - - if !p.isSpawn { - // PlayerPositionAndRotation to confirm the spawn position - err = p.c.Conn.WritePacket(pk.Marshal( - packetid.ServerboundMovePlayerPosRot, - X, Y-1.62, Z, - Yaw, Pitch, - pk.Boolean(true), - )) - if err != nil { - return Error{err} - } - p.isSpawn = true - } - - return nil -} diff --git a/bot/msg/chat.go b/bot/msg/chat.go index d67d90f..26970c5 100644 --- a/bot/msg/chat.go +++ b/bot/msg/chat.go @@ -17,6 +17,7 @@ import ( pk "github.com/Tnze/go-mc/net/packet" ) +// The Manager is used to receive and send chat messages. type Manager struct { c *bot.Client p *basic.Player @@ -26,6 +27,7 @@ type Manager struct { sign.SignatureCache } +// New returns a new chat manager. func New(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, events EventsHandler) *Manager { m := &Manager{ c: c, @@ -37,7 +39,7 @@ func New(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, events Event if events.SystemChat != nil { c.Events.AddListener(bot.PacketHandler{ Priority: 64, ID: packetid.ClientboundSystemChat, - F: m.handleSystemMessage, + F: m.handleSystemChat, }) } if events.PlayerChatMessage != nil { @@ -55,7 +57,7 @@ func New(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, events Event return m } -func (m *Manager) handleSystemMessage(p pk.Packet) error { +func (m *Manager) handleSystemChat(p pk.Packet) error { var msg chat.Message var overlay pk.Boolean if err := p.Scan(&msg, &overlay); err != nil { @@ -149,7 +151,7 @@ func (m *Manager) handleDisguisedChat(packet pk.Packet) error { } // SendMessage send chat message to server. -// Currently only support offline-mode or "Not Secure" chat +// Doesn't support sending message with signature currently. func (m *Manager) SendMessage(msg string) error { if len(msg) > 256 { return errors.New("message length greater than 256") diff --git a/bot/msg/events.go b/bot/msg/events.go index aa95131..7353e3e 100644 --- a/bot/msg/events.go +++ b/bot/msg/events.go @@ -2,8 +2,28 @@ package msg import "github.com/Tnze/go-mc/chat" +// EventsHandler is a collection of event handlers. +// Fill the fields with your handler functions and pass this struct to [New] to create the msg manager. +// The handler functions will be called when the corresponding event is triggered. +// Leave the fields as nil if you don't want to handle the event. type EventsHandler struct { - SystemChat func(msg chat.Message, overlay bool) error + // SystemChat handles messages sent by gaming system. + // + // In vanilla client: + // If overlay is false, the message will be displayed in the chat box. + // If overlay is true, the message will be displayed on the top of the hot-bar. + SystemChat func(msg chat.Message, overlay bool) error + + // PlayerChatMessage handles messages sent by players. + // + // Message signing system is added in 1.19. The message and its context could be signed by the player's private key. + // The manager tries to verify the message signature through the player's public key, + // and return the result as validated boolean. PlayerChatMessage func(msg chat.Message, validated bool) error - DisguisedChat func(msg chat.Message) error + + // DisguisedChat handles DisguisedChat message. + // + // DisguisedChat message used to send system chat. + // Now it is used to send messages from "/say" command from server console. + DisguisedChat func(msg chat.Message) error } diff --git a/bot/playerlist/playerlist.go b/bot/playerlist/playerlist.go index 2b235ef..3c3c343 100644 --- a/bot/playerlist/playerlist.go +++ b/bot/playerlist/playerlist.go @@ -1,3 +1,9 @@ +// Package playerlist contains a PlayerList struct that used to manage player information. +// +// The [PlayerList] contains a list of [PlayerInfo] which is received from server when client join. +// The playerlist contains every players' information of name, display name, uuid, gamemode, latency, public key, etc. +// And can be used to render the "TAB List". Other packages may also require playerlist to work, +// for example, the bot/msg package. package playerlist import (