remove bot and message.go
This commit is contained in:
@ -1,91 +0,0 @@
|
||||
// Package basic provides some basic packet handler which client needs.
|
||||
//
|
||||
// # [Player]
|
||||
//
|
||||
// The [Player] is attached to a [Client] by calling [NewPlayer] before the client joins a server.
|
||||
//
|
||||
// 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.
|
||||
// - Respawn, for updating player info, which may change when player respawned.
|
||||
//
|
||||
// # [EventsListener]
|
||||
//
|
||||
// Handles some basic event you probably need.
|
||||
// - GameStart
|
||||
// - Disconnect
|
||||
// - HealthChange
|
||||
// - Death
|
||||
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
|
||||
}
|
||||
|
||||
// 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.ClientboundRespawn, F: p.handleRespawnPacket},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundPing, F: p.handlePingPacket},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundCookieRequest, F: p.handleCookieRequestPacket},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundStoreCookie, F: p.handleStoreCookiePacket},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundUpdateTags, F: p.handleUpdateTags},
|
||||
)
|
||||
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
|
||||
|
||||
err := p.c.Conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundClientCommand,
|
||||
pk.VarInt(PerformRespawn),
|
||||
))
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return "bot/basic: " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
func (p *Player) handleCookieRequestPacket(packet pk.Packet) error {
|
||||
var key pk.Identifier
|
||||
err := packet.Scan(&key)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
cookieContent := p.c.Cookies[string(key)]
|
||||
err = p.c.Conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundCookieResponse,
|
||||
key, pk.OptionEncoder[pk.ByteArray]{
|
||||
Has: cookieContent != nil,
|
||||
Val: pk.ByteArray(cookieContent),
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Player) handleStoreCookiePacket(packet pk.Packet) error {
|
||||
var key pk.Identifier
|
||||
var payload pk.ByteArray
|
||||
err := packet.Scan(&key, &payload)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
p.c.Cookies[string(key)] = []byte(payload)
|
||||
return nil
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 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) error
|
||||
}
|
||||
|
||||
// attach your event listener to the client.
|
||||
// The functions are copied when attaching, and modify on [EventListener] doesn't affect after that.
|
||||
func (e EventsListener) attach(p *Player) {
|
||||
if e.GameStart != nil {
|
||||
attachJoinGameHandler(p.c, e.GameStart)
|
||||
}
|
||||
if e.Disconnect != nil {
|
||||
attachDisconnect(p.c, e.Disconnect)
|
||||
}
|
||||
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) {
|
||||
c.Events.AddListener(bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundLogin,
|
||||
F: func(_ pk.Packet) error {
|
||||
return handler()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func attachDisconnect(c *bot.Client, handler func(reason chat.Message) error) {
|
||||
c.Events.AddListener(bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundDisconnect,
|
||||
F: func(p pk.Packet) error {
|
||||
var reason chat.Message
|
||||
if err := p.Scan(&reason); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return handler(chat.Message(reason))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 saturation pk.Float
|
||||
|
||||
if err := p.Scan(&health, &food, &saturation); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
var healthChangeErr, deathErr error
|
||||
if healthChangeHandler != nil {
|
||||
healthChangeErr = healthChangeHandler(float32(health), int32(food), float32(saturation))
|
||||
}
|
||||
if deathHandler != nil && health <= 0 {
|
||||
deathErr = deathHandler()
|
||||
}
|
||||
if healthChangeErr != nil || deathErr != nil {
|
||||
return updateHealthError{healthChangeErr, deathErr}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func attachPlayerPosition(c *bot.Client, handler func(x, y, z float64, yaw, pitch float32, flag byte, teleportID int32) 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
|
||||
)
|
||||
if err := p.Scan(&X, &Y, &Z, &Yaw, &Pitch, &Flags, &TeleportID); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return handler(float64(X), float64(Y), float64(Z), float32(Yaw), float32(Pitch), byte(Flags), int32(TeleportID))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type updateHealthError struct {
|
||||
healthChangeErr, deathErr error
|
||||
}
|
||||
|
||||
func (u updateHealthError) Unwrap() error {
|
||||
if u.healthChangeErr != nil {
|
||||
return u.healthChangeErr
|
||||
}
|
||||
if u.deathErr != nil {
|
||||
return u.deathErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u updateHealthError) Error() string {
|
||||
switch {
|
||||
case u.healthChangeErr != nil && u.deathErr != nil:
|
||||
return "[" + u.healthChangeErr.Error() + ", " + u.deathErr.Error() + "]"
|
||||
case u.healthChangeErr != nil:
|
||||
return u.healthChangeErr.Error()
|
||||
case u.deathErr != nil:
|
||||
return u.deathErr.Error()
|
||||
default:
|
||||
return "nil"
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
// WorldInfo content player info in server.
|
||||
type WorldInfo struct {
|
||||
DimensionType int32
|
||||
DimensionNames []string // Identifiers for all worlds on the server.
|
||||
DimensionName 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).
|
||||
SimulationDistance int32 // The distance that the client will process specific things, such as entities.
|
||||
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.
|
||||
DoLimitCrafting bool // Whether players can only craft recipes they have already unlocked. Currently unused by the client.
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p *Player) handleLoginPacket(packet pk.Packet) error {
|
||||
err := packet.Scan(
|
||||
(*pk.Int)(&p.EID),
|
||||
(*pk.Boolean)(&p.Hardcore),
|
||||
pk.Array((*[]pk.Identifier)(unsafe.Pointer(&p.DimensionNames))),
|
||||
(*pk.VarInt)(&p.MaxPlayers),
|
||||
(*pk.VarInt)(&p.ViewDistance),
|
||||
(*pk.VarInt)(&p.SimulationDistance),
|
||||
(*pk.Boolean)(&p.ReducedDebugInfo),
|
||||
(*pk.Boolean)(&p.EnableRespawnScreen),
|
||||
(*pk.Boolean)(&p.DoLimitCrafting),
|
||||
(*pk.VarInt)(&p.WorldInfo.DimensionType),
|
||||
(*pk.Identifier)(&p.DimensionName),
|
||||
(*pk.Long)(&p.HashedSeed),
|
||||
(*pk.UnsignedByte)(&p.Gamemode),
|
||||
(*pk.Byte)(&p.PrevGamemode),
|
||||
(*pk.Boolean)(&p.IsDebug),
|
||||
(*pk.Boolean)(&p.IsFlat),
|
||||
// Death dimension & Death location & Portal cooldown are ignored
|
||||
)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
err = p.c.Conn.WritePacket(pk.Marshal( // PluginMessage packet
|
||||
packetid.ServerboundCustomPayload,
|
||||
pk.Identifier("minecraft:brand"),
|
||||
pk.String(p.Settings.Brand),
|
||||
))
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
err = p.c.Conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundClientInformation, // Client settings
|
||||
pk.String(p.Settings.Locale),
|
||||
pk.Byte(p.Settings.ViewDistance),
|
||||
pk.VarInt(p.Settings.ChatMode),
|
||||
pk.Boolean(p.Settings.ChatColors),
|
||||
pk.UnsignedByte(p.Settings.DisplayedSkinParts),
|
||||
pk.VarInt(p.Settings.MainHand),
|
||||
pk.Boolean(p.Settings.EnableTextFiltering),
|
||||
pk.Boolean(p.Settings.AllowListing),
|
||||
))
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
p.resetKeepAliveDeadline()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Player) handleRespawnPacket(packet pk.Packet) error {
|
||||
var copyMeta bool
|
||||
err := packet.Scan(
|
||||
(*pk.VarInt)(&p.DimensionType),
|
||||
(*pk.Identifier)(&p.DimensionName),
|
||||
(*pk.Long)(&p.HashedSeed),
|
||||
(*pk.UnsignedByte)(&p.Gamemode),
|
||||
(*pk.Byte)(&p.PrevGamemode),
|
||||
(*pk.Boolean)(&p.IsDebug),
|
||||
(*pk.Boolean)(&p.IsFlat),
|
||||
(*pk.Boolean)(©Meta),
|
||||
)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
const keepAliveDuration = time.Second * 20
|
||||
|
||||
func (p *Player) resetKeepAliveDeadline() {
|
||||
newDeadline := time.Now().Add(keepAliveDuration)
|
||||
p.c.Conn.Socket.SetDeadline(newDeadline)
|
||||
}
|
||||
|
||||
func (p *Player) handleKeepAlivePacket(packet pk.Packet) error {
|
||||
var KeepAliveID pk.Long
|
||||
if err := packet.Scan(&KeepAliveID); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
p.resetKeepAliveDeadline()
|
||||
|
||||
// Response
|
||||
err := p.c.Conn.WritePacket(pk.Packet{
|
||||
ID: int32(packetid.ServerboundKeepAlive),
|
||||
Data: packet.Data,
|
||||
})
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
func (p *Player) handlePingPacket(packet pk.Packet) error {
|
||||
var pingID pk.Int
|
||||
if err := packet.Scan(&pingID); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
// Response
|
||||
err := p.c.Conn.WritePacket(pk.Packet{
|
||||
ID: int32(packetid.ServerboundPong),
|
||||
Data: packet.Data,
|
||||
})
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package basic
|
||||
|
||||
// Settings of client
|
||||
type Settings struct {
|
||||
Locale string // 地区
|
||||
ViewDistance int // 视距
|
||||
ChatMode int // 聊天模式
|
||||
ChatColors bool // 聊天颜色
|
||||
DisplayedSkinParts uint8 // 皮肤显示
|
||||
MainHand int // 主手
|
||||
|
||||
// Enables filtering of text on signs and written book titles.
|
||||
// Currently, always false (i.e. the filtering is disabled)
|
||||
EnableTextFiltering bool
|
||||
AllowListing bool
|
||||
|
||||
// The brand string presented to the server.
|
||||
Brand string
|
||||
}
|
||||
|
||||
// Used by Settings.DisplayedSkinParts.
|
||||
// For each bit set if shows match part.
|
||||
const (
|
||||
_ = 1 << iota
|
||||
Jacket
|
||||
LeftSleeve
|
||||
RightSleeve
|
||||
LeftPantsLeg
|
||||
RightPantsLeg
|
||||
Hat
|
||||
)
|
||||
|
||||
// DefaultSettings are the default settings of client
|
||||
var DefaultSettings = Settings{
|
||||
Locale: "zh_CN", // ^_^
|
||||
ViewDistance: 15,
|
||||
ChatMode: 0,
|
||||
DisplayedSkinParts: Jacket | LeftSleeve | RightSleeve | LeftPantsLeg | RightPantsLeg | Hat,
|
||||
MainHand: 1,
|
||||
|
||||
EnableTextFiltering: false,
|
||||
AllowListing: true,
|
||||
|
||||
Brand: "vanilla",
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
func (p *Player) handleUpdateTags(packet pk.Packet) error {
|
||||
r := bytes.NewReader(packet.Data)
|
||||
|
||||
var length pk.VarInt
|
||||
_, err := length.ReadFrom(r)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
var registryID pk.Identifier
|
||||
for i := 0; i < int(length); i++ {
|
||||
_, err = registryID.ReadFrom(r)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
registry := p.c.Registries.Registry(string(registryID))
|
||||
if registry == nil {
|
||||
return Error{errors.New("unknown registry: " + string(registryID))}
|
||||
}
|
||||
|
||||
_, err = registry.ReadTagsFrom(r)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
136
bot/client.go
136
bot/client.go
@ -1,136 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
"github.com/Tnze/go-mc/net"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
"github.com/Tnze/go-mc/net/queue"
|
||||
"github.com/Tnze/go-mc/registry"
|
||||
)
|
||||
|
||||
// Client is used to access Minecraft server
|
||||
type Client struct {
|
||||
Conn *Conn
|
||||
Auth Auth
|
||||
|
||||
// These are filled when login process
|
||||
Name string
|
||||
UUID uuid.UUID
|
||||
Registries registry.Registries
|
||||
Cookies map[string][]byte
|
||||
|
||||
// Ingame packet handlers
|
||||
Events Events
|
||||
|
||||
// Login plugins
|
||||
LoginPlugin map[string]CustomPayloadHandler
|
||||
|
||||
// Configuration handler
|
||||
ConfigHandler
|
||||
|
||||
CustomReportDetails map[string]string
|
||||
}
|
||||
|
||||
// CustomPayloadHandler is a function handling custom payload
|
||||
type CustomPayloadHandler func(data []byte) ([]byte, error)
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// NewClient init and return a new Client.
|
||||
//
|
||||
// A new Client has default name "Steve" and zero UUID.
|
||||
// It is usable for an offline-mode game.
|
||||
//
|
||||
// For online-mode, you need login your Mojang account
|
||||
// and load your Name, UUID and AccessToken to client.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
Auth: Auth{Name: "Steve"},
|
||||
Registries: registry.NewNetworkCodec(),
|
||||
Events: Events{handlers: make([][]PacketHandler, packetid.ClientboundPacketIDGuard)},
|
||||
LoginPlugin: make(map[string]CustomPayloadHandler),
|
||||
ConfigHandler: NewDefaultConfigHandler(),
|
||||
CustomReportDetails: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Conn is a concurrently-safe warpper of net.Conn with packet queue.
|
||||
// Note that not all methods are concurrently-safe.
|
||||
type Conn struct {
|
||||
*net.Conn
|
||||
send, recv queue.Queue[pk.Packet]
|
||||
pool sync.Pool // pool of recv packet data
|
||||
rerr error
|
||||
}
|
||||
|
||||
func warpConn(c *net.Conn, qr, qw queue.Queue[pk.Packet]) *Conn {
|
||||
wc := Conn{
|
||||
Conn: c,
|
||||
send: qw,
|
||||
recv: qr,
|
||||
pool: sync.Pool{New: func() any { return []byte{} }},
|
||||
rerr: nil,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
// take a buffer from pool, after the packet is handled we put it back
|
||||
p := pk.Packet{Data: wc.pool.Get().([]byte)}
|
||||
if err := c.ReadPacket(&p); err != nil {
|
||||
wc.rerr = err
|
||||
break
|
||||
}
|
||||
if ok := wc.recv.Push(p); !ok {
|
||||
wc.rerr = errors.New("receive queue is full")
|
||||
break
|
||||
}
|
||||
}
|
||||
wc.recv.Close()
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
p, ok := wc.send.Pull()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if err := c.WritePacket(p); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &wc
|
||||
}
|
||||
|
||||
func (c *Conn) ReadPacket(p *pk.Packet) error {
|
||||
packet, ok := c.recv.Pull()
|
||||
if !ok {
|
||||
return c.rerr
|
||||
}
|
||||
*p = packet
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) WritePacket(p pk.Packet) error {
|
||||
ok := c.send.Push(p)
|
||||
if !ok {
|
||||
return errors.New("queue is full")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.send.Close()
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// Position is a 3D vector.
|
||||
type Position struct {
|
||||
X, Y, Z int
|
||||
}
|
@ -1,396 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
"github.com/Tnze/go-mc/net"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
type ConfigHandler interface {
|
||||
EnableFeature(features []pk.Identifier)
|
||||
|
||||
PushResourcePack(res ResourcePack)
|
||||
PopResourcePack(id pk.UUID)
|
||||
PopAllResourcePack()
|
||||
|
||||
SelectDataPacks(packs []DataPack) []DataPack
|
||||
}
|
||||
|
||||
type ResourcePack struct {
|
||||
ID pk.UUID
|
||||
URL string
|
||||
Hash string
|
||||
Forced bool
|
||||
PromptMessage *chat.Message // Optional
|
||||
}
|
||||
|
||||
type ConfigErr struct {
|
||||
Stage string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (l ConfigErr) Error() string {
|
||||
return "bot: configuration error: [" + l.Stage + "] " + l.Err.Error()
|
||||
}
|
||||
|
||||
func (l ConfigErr) Unwrap() error {
|
||||
return l.Err
|
||||
}
|
||||
|
||||
func (c *Client) joinConfiguration(conn *net.Conn) error {
|
||||
for {
|
||||
var p pk.Packet
|
||||
if err := conn.ReadPacket(&p); err != nil {
|
||||
return ConfigErr{"config custom payload", err}
|
||||
}
|
||||
|
||||
switch packetid.ClientboundPacketID(p.ID) {
|
||||
case packetid.ClientboundConfigCookieRequest:
|
||||
var key pk.Identifier
|
||||
err := p.Scan(&key)
|
||||
if err != nil {
|
||||
return ConfigErr{"cookie request", err}
|
||||
}
|
||||
cookieContent := c.Cookies[string(key)]
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundConfigCookieResponse,
|
||||
key, pk.OptionEncoder[pk.ByteArray]{
|
||||
Has: cookieContent != nil,
|
||||
Val: pk.ByteArray(cookieContent),
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
return ConfigErr{"cookie response", err}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigCustomPayload:
|
||||
var channel pk.Identifier
|
||||
var data pk.PluginMessageData
|
||||
err := p.Scan(&channel, &data)
|
||||
if err != nil {
|
||||
return ConfigErr{"custom payload", err}
|
||||
}
|
||||
// TODO: Provide configuration custom data handling interface
|
||||
//
|
||||
// There are two types of Custom packet.
|
||||
// One for Login stage, the other for config and play stage.
|
||||
// The first one called "Custom Query", and the second one called "Custom Payload".
|
||||
// We can know the different by their name, the "query" is one request to one response, paired.
|
||||
// But the second one can be sent in any order.
|
||||
//
|
||||
// And the custome payload packet seems to be same in config stage and play stage.
|
||||
// How do we provide API for that?
|
||||
|
||||
case packetid.ClientboundConfigDisconnect:
|
||||
const ErrStage = "disconnect"
|
||||
var reason chat.Message
|
||||
err := p.Scan(&reason)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
return ConfigErr{ErrStage, DisconnectErr(reason)}
|
||||
|
||||
case packetid.ClientboundConfigFinishConfiguration:
|
||||
err := conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundConfigFinishConfiguration,
|
||||
))
|
||||
if err != nil {
|
||||
return ConfigErr{"finish config", err}
|
||||
}
|
||||
return nil
|
||||
|
||||
case packetid.ClientboundConfigKeepAlive:
|
||||
const ErrStage = "keep alive"
|
||||
var keepAliveID pk.Long
|
||||
err := p.Scan(&keepAliveID)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
// send it back
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundConfigKeepAlive,
|
||||
keepAliveID,
|
||||
))
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigPing:
|
||||
var pingID pk.Int
|
||||
err := p.Scan(&pingID)
|
||||
if err != nil {
|
||||
return ConfigErr{"ping", err}
|
||||
}
|
||||
// send it back
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundConfigPong,
|
||||
pingID,
|
||||
))
|
||||
if err != nil {
|
||||
return ConfigErr{"pong", err}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigResetChat:
|
||||
// TODO
|
||||
|
||||
case packetid.ClientboundConfigRegistryData:
|
||||
const ErrStage = "registry"
|
||||
var registryID pk.Identifier
|
||||
|
||||
r := bytes.NewReader(p.Data)
|
||||
_, err := registryID.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
|
||||
registry := c.Registries.Registry(string(registryID))
|
||||
if registry == nil {
|
||||
return ConfigErr{ErrStage, errors.New("unknown registry: " + string(registryID))}
|
||||
}
|
||||
|
||||
_, err = registry.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, fmt.Errorf("failed to read registry %s: %w", registryID, err)}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigResourcePackPop:
|
||||
var id pk.Option[pk.UUID, *pk.UUID]
|
||||
err := p.Scan(&id)
|
||||
if err != nil {
|
||||
return ConfigErr{"resource pack pop", err}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigResourcePackPush:
|
||||
var id pk.UUID
|
||||
var Url, Hash pk.String
|
||||
var Forced pk.Boolean
|
||||
var PromptMessage pk.Option[chat.Message, *chat.Message]
|
||||
err := p.Scan(
|
||||
&id,
|
||||
&Url,
|
||||
&Hash,
|
||||
&Forced,
|
||||
&PromptMessage,
|
||||
)
|
||||
if err != nil {
|
||||
return ConfigErr{"resource pack", err}
|
||||
}
|
||||
res := ResourcePack{
|
||||
ID: id,
|
||||
URL: string(Url),
|
||||
Hash: string(Hash),
|
||||
Forced: bool(Forced),
|
||||
}
|
||||
if PromptMessage.Has {
|
||||
res.PromptMessage = &PromptMessage.Val
|
||||
}
|
||||
c.ConfigHandler.PushResourcePack(res)
|
||||
|
||||
case packetid.ClientboundConfigStoreCookie:
|
||||
var key pk.Identifier
|
||||
var payload pk.ByteArray
|
||||
err := p.Scan(&key, &payload)
|
||||
if err != nil {
|
||||
return ConfigErr{"store cookie", err}
|
||||
}
|
||||
c.Cookies[string(key)] = []byte(payload)
|
||||
|
||||
case packetid.ClientboundConfigTransfer:
|
||||
var host pk.String
|
||||
var port pk.VarInt
|
||||
err := p.Scan(&host, &port)
|
||||
if err != nil {
|
||||
return ConfigErr{"transfer", err}
|
||||
}
|
||||
// TODO: trnasfer to the specific server
|
||||
// How does it work? Just connect the new server, and re-start at handshake?
|
||||
|
||||
case packetid.ClientboundConfigUpdateEnabledFeatures:
|
||||
features := []pk.Identifier{}
|
||||
err := p.Scan(pk.Array(&features))
|
||||
if err != nil {
|
||||
return ConfigErr{"update enabled features", err}
|
||||
}
|
||||
c.ConfigHandler.EnableFeature(features)
|
||||
|
||||
case packetid.ClientboundConfigUpdateTags:
|
||||
const ErrStage = "update tags"
|
||||
r := bytes.NewReader(p.Data)
|
||||
|
||||
var length pk.VarInt
|
||||
_, err := length.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
|
||||
var registryID pk.Identifier
|
||||
for i := 0; i < int(length); i++ {
|
||||
_, err = registryID.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
|
||||
registry := c.Registries.Registry(string(registryID))
|
||||
if registry == nil {
|
||||
// TODO: Sice our registry system is incompelted, ignore all tags bind to non-exist registry
|
||||
_, err = idleTagsDecoder{}.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
continue
|
||||
// return ConfigErr{ErrStage, errors.New("unknown registry: " + string(registryID))}
|
||||
}
|
||||
|
||||
_, err = registry.ReadTagsFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigSelectKnownPacks:
|
||||
const ErrStage = "select known packs"
|
||||
packs := []DataPack{}
|
||||
err := p.Scan(pk.Array(&packs))
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
knwonPacks := c.ConfigHandler.SelectDataPacks(packs)
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundConfigSelectKnownPacks,
|
||||
pk.Array(knwonPacks),
|
||||
))
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigCustomReportDetails:
|
||||
const ErrStage = "custom report details"
|
||||
var length pk.VarInt
|
||||
var title, description pk.String
|
||||
r := bytes.NewReader(p.Data)
|
||||
_, err := length.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
for i := 0; i < int(length); i++ {
|
||||
_, err = title.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
_, err = description.ReadFrom(r)
|
||||
if err != nil {
|
||||
return ConfigErr{ErrStage, err}
|
||||
}
|
||||
c.CustomReportDetails[string(title)] = string(description)
|
||||
}
|
||||
|
||||
case packetid.ClientboundConfigServerLinks:
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DataPack struct {
|
||||
Namespace string
|
||||
ID string
|
||||
Version string
|
||||
}
|
||||
|
||||
func (d DataPack) WriteTo(w io.Writer) (n int64, err error) {
|
||||
n, err = pk.String(d.Namespace).WriteTo(w)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n1, err := pk.String(d.ID).WriteTo(w)
|
||||
if err != nil {
|
||||
return n + n1, err
|
||||
}
|
||||
n2, err := pk.String(d.Version).WriteTo(w)
|
||||
return n + n1 + n2, err
|
||||
}
|
||||
|
||||
func (d *DataPack) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
n, err = (*pk.String)(&d.Namespace).ReadFrom(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n1, err := (*pk.String)(&d.ID).ReadFrom(r)
|
||||
if err != nil {
|
||||
return n + n1, err
|
||||
}
|
||||
n2, err := (*pk.String)(&d.Version).ReadFrom(r)
|
||||
return n + n1 + n2, err
|
||||
}
|
||||
|
||||
type DefaultConfigHandler struct {
|
||||
resourcesPack []ResourcePack
|
||||
}
|
||||
|
||||
func NewDefaultConfigHandler() *DefaultConfigHandler {
|
||||
return &DefaultConfigHandler{
|
||||
resourcesPack: make([]ResourcePack, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultConfigHandler) EnableFeature(features []pk.Identifier) {}
|
||||
|
||||
func (d *DefaultConfigHandler) PushResourcePack(res ResourcePack) {
|
||||
d.resourcesPack = append(d.resourcesPack, res)
|
||||
}
|
||||
|
||||
func (d *DefaultConfigHandler) PopResourcePack(id pk.UUID) {
|
||||
for i, v := range d.resourcesPack {
|
||||
if id == v.ID {
|
||||
d.resourcesPack = append(d.resourcesPack[:i], d.resourcesPack[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultConfigHandler) PopAllResourcePack() {
|
||||
d.resourcesPack = d.resourcesPack[:0]
|
||||
}
|
||||
|
||||
func (d *DefaultConfigHandler) SelectDataPacks(packs []DataPack) []DataPack {
|
||||
return []DataPack{}
|
||||
}
|
||||
|
||||
type idleTagsDecoder struct{}
|
||||
|
||||
func (idleTagsDecoder) ReadFrom(r io.Reader) (int64, error) {
|
||||
var count pk.VarInt
|
||||
var tag pk.Identifier
|
||||
var length pk.VarInt
|
||||
n, err := count.ReadFrom(r)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
for i := 0; i < int(count); i++ {
|
||||
var n1, n2, n3 int64
|
||||
n1, err = tag.ReadFrom(r)
|
||||
if err != nil {
|
||||
return n + n1, err
|
||||
}
|
||||
n2, err = length.ReadFrom(r)
|
||||
if err != nil {
|
||||
return n + n1 + n2, err
|
||||
}
|
||||
n += n1 + n2
|
||||
|
||||
var id pk.VarInt
|
||||
for i := 0; i < int(length); i++ {
|
||||
n3, err = id.ReadFrom(r)
|
||||
if err != nil {
|
||||
return n + n3, err
|
||||
}
|
||||
n += n3
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
51
bot/event.go
51
bot/event.go
@ -1,51 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
type Events struct {
|
||||
generic []PacketHandler // for every packet
|
||||
handlers [][]PacketHandler // for specific packet id only
|
||||
}
|
||||
|
||||
func (e *Events) AddListener(listeners ...PacketHandler) {
|
||||
for _, l := range listeners {
|
||||
// panic if l.ID is invalid
|
||||
if l.ID < 0 || int(l.ID) >= len(e.handlers) {
|
||||
panic("Invalid packet ID (" + strconv.Itoa(int(l.ID)) + ")")
|
||||
}
|
||||
if s := e.handlers[l.ID]; s == nil {
|
||||
e.handlers[l.ID] = []PacketHandler{l}
|
||||
} else {
|
||||
e.handlers[l.ID] = append(s, l)
|
||||
sortPacketHandlers(e.handlers[l.ID])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddGeneric adds listeners like AddListener, but the packet ID is ignored.
|
||||
// Generic listener is always called before specific packet listener.
|
||||
func (e *Events) AddGeneric(listeners ...PacketHandler) {
|
||||
e.generic = append(e.generic, listeners...)
|
||||
sortPacketHandlers(e.generic)
|
||||
}
|
||||
|
||||
type (
|
||||
PacketHandlerFunc func(p pk.Packet) error
|
||||
PacketHandler struct {
|
||||
ID packetid.ClientboundPacketID
|
||||
Priority int
|
||||
F func(p pk.Packet) error
|
||||
}
|
||||
)
|
||||
|
||||
func sortPacketHandlers(slice []PacketHandler) {
|
||||
sort.SliceStable(slice, func(i, j int) bool {
|
||||
return slice[i].Priority > slice[j].Priority
|
||||
})
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"log"
|
||||
|
||||
"github.com/Tnze/go-mc/offline"
|
||||
"github.com/Tnze/go-mc/yggdrasil"
|
||||
)
|
||||
|
||||
func ExamplePingAndList() {
|
||||
resp, delay, err := PingAndList("localhost:25565")
|
||||
if err != nil {
|
||||
log.Fatalf("ping and list server fail: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Status:", string(resp))
|
||||
log.Println("Delay:", delay)
|
||||
}
|
||||
|
||||
func ExampleClient_JoinServer_offline() {
|
||||
c := NewClient()
|
||||
c.Auth.Name = "Tnze" // set its name before login.
|
||||
|
||||
id := offline.NameToUUID(c.Auth.Name) // optional, get uuid of offline mode game
|
||||
c.Auth.UUID = hex.EncodeToString(id[:])
|
||||
|
||||
// Login
|
||||
err := c.JoinServer("127.0.0.1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Login success")
|
||||
|
||||
// Register event handlers
|
||||
// c.Events.AddListener(...)
|
||||
|
||||
// JoinGame
|
||||
err = c.HandleGame()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_JoinServer_online() {
|
||||
c := NewClient()
|
||||
|
||||
// Login Mojang account to get AccessToken
|
||||
// To use Microsoft Account, see issue #106
|
||||
// https://github.com/Tnze/go-mc/issues/106
|
||||
auth, err := yggdrasil.Authenticate("Your E-mail", "Your Password")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// As long as you set these three fields correctly,
|
||||
// the client can connect to the online-mode server
|
||||
c.Auth.UUID, c.Auth.Name = auth.SelectedProfile()
|
||||
c.Auth.AsTk = auth.AccessToken()
|
||||
|
||||
// Connect server
|
||||
err = c.JoinServer("127.0.0.1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Login success")
|
||||
|
||||
// Register event handlers
|
||||
// c.Events.GameStart = onGameStartFunc
|
||||
// c.Events.ChatMsg = onChatMsgFunc
|
||||
// c.Events.Disconnect = onDisconnectFunc
|
||||
// ...
|
||||
|
||||
// Join the game
|
||||
err = c.HandleGame()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
for {
|
||||
var p pk.Packet
|
||||
// Read packets
|
||||
if err := c.Conn.ReadPacket(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.ID == int32(packetid.BundleDelimiter) {
|
||||
err := c.handleBundlePackets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// handle packets
|
||||
err := c.handlePacket(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return the packet buffer
|
||||
c.Conn.pool.Put(p.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PacketHandlerError struct {
|
||||
ID packetid.ClientboundPacketID
|
||||
Err error
|
||||
}
|
||||
|
||||
func (d PacketHandlerError) Error() string {
|
||||
return fmt.Sprintf("handle packet %v error: %v", d.ID, d.Err)
|
||||
}
|
||||
|
||||
func (d PacketHandlerError) Unwrap() error {
|
||||
return d.Err
|
||||
}
|
||||
|
||||
func (c *Client) handleBundlePackets() (err error) {
|
||||
var packets []pk.Packet
|
||||
for i := 0; i < 4096; i++ {
|
||||
var p pk.Packet
|
||||
// Read packets
|
||||
if err := c.Conn.ReadPacket(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.ID == int32(packetid.BundleDelimiter) {
|
||||
// bundle finished
|
||||
goto handlePackets
|
||||
}
|
||||
|
||||
packets = append(packets, p)
|
||||
}
|
||||
return errors.New("packet number of a bundle out of limit")
|
||||
|
||||
handlePackets:
|
||||
for i := range packets {
|
||||
if err := c.handlePacket(packets[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handlePacket(p pk.Packet) (err error) {
|
||||
packetID := packetid.ClientboundPacketID(p.ID)
|
||||
for _, handler := range c.Events.generic {
|
||||
if err = handler.F(p); err != nil {
|
||||
return PacketHandlerError{ID: packetID, Err: err}
|
||||
}
|
||||
}
|
||||
for _, handler := range c.Events.handlers[packetID] {
|
||||
err = handler.F(p)
|
||||
if err != nil {
|
||||
return PacketHandlerError{ID: packetID, Err: err}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
328
bot/login.go
328
bot/login.go
@ -1,328 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
"github.com/Tnze/go-mc/net"
|
||||
"github.com/Tnze/go-mc/net/CFB8"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
type LoginErr struct {
|
||||
Stage string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (l LoginErr) Error() string {
|
||||
return "bot: login error: [" + l.Stage + "] " + l.Err.Error()
|
||||
}
|
||||
|
||||
func (l LoginErr) Unwrap() error {
|
||||
return l.Err
|
||||
}
|
||||
|
||||
func (c *Client) joinLogin(conn *net.Conn) error {
|
||||
var err error
|
||||
if c.Auth.UUID != "" {
|
||||
c.UUID, err = uuid.Parse(c.Auth.UUID)
|
||||
if err != nil {
|
||||
return LoginErr{"login start", err}
|
||||
}
|
||||
}
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundLoginHello,
|
||||
pk.String(c.Auth.Name),
|
||||
pk.UUID(c.UUID),
|
||||
))
|
||||
if err != nil {
|
||||
return LoginErr{"login start", err}
|
||||
}
|
||||
receiving := "encrypt start"
|
||||
for {
|
||||
// Receive Packet
|
||||
var p pk.Packet
|
||||
if err = conn.ReadPacket(&p); err != nil {
|
||||
return LoginErr{receiving, err}
|
||||
}
|
||||
|
||||
// Handle Packet
|
||||
switch packetid.ClientboundPacketID(p.ID) {
|
||||
case packetid.ClientboundLoginLoginDisconnect: // LoginDisconnect
|
||||
var reason chat.JsonMessage
|
||||
err = p.Scan(&reason)
|
||||
if err != nil {
|
||||
return LoginErr{"disconnect", err}
|
||||
}
|
||||
return LoginErr{"disconnect", DisconnectErr(reason)}
|
||||
|
||||
case packetid.ClientboundLoginHello: // Encryption Request
|
||||
if err := handleEncryptionRequest(conn, c, p); err != nil {
|
||||
return LoginErr{"encryption", err}
|
||||
}
|
||||
receiving = "set compression"
|
||||
|
||||
case packetid.ClientboundLoginGameProfile: // Login Success
|
||||
err := p.Scan(
|
||||
(*pk.UUID)(&c.UUID),
|
||||
(*pk.String)(&c.Name),
|
||||
)
|
||||
if err != nil {
|
||||
return LoginErr{"login success", err}
|
||||
}
|
||||
err = conn.WritePacket(pk.Marshal(packetid.ServerboundLoginLoginAcknowledged))
|
||||
if err != nil {
|
||||
return LoginErr{"login success", err}
|
||||
}
|
||||
return nil
|
||||
|
||||
case packetid.ClientboundLoginLoginCompression: // Set Compression
|
||||
var threshold pk.VarInt
|
||||
if err := p.Scan(&threshold); err != nil {
|
||||
return LoginErr{"compression", err}
|
||||
}
|
||||
conn.SetThreshold(int(threshold))
|
||||
receiving = "login success"
|
||||
|
||||
case packetid.ClientboundLoginCustomQuery: // Login Plugin Request
|
||||
var (
|
||||
msgid pk.VarInt
|
||||
channel pk.Identifier
|
||||
data pk.PluginMessageData
|
||||
)
|
||||
if err := p.Scan(&msgid, &channel, &data); err != nil {
|
||||
return LoginErr{"Login Plugin", err}
|
||||
}
|
||||
|
||||
var PluginMessageData pk.Option[pk.PluginMessageData, *pk.PluginMessageData]
|
||||
if handler, ok := c.LoginPlugin[string(channel)]; ok {
|
||||
PluginMessageData.Has = true
|
||||
PluginMessageData.Val, err = handler(data)
|
||||
if err != nil {
|
||||
return LoginErr{"Login Plugin", err}
|
||||
}
|
||||
}
|
||||
|
||||
if err := conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundLoginCustomQueryAnswer,
|
||||
msgid, PluginMessageData,
|
||||
)); err != nil {
|
||||
return LoginErr{"login Plugin", err}
|
||||
}
|
||||
|
||||
case packetid.ClientboundLoginCookieRequest:
|
||||
var key pk.Identifier
|
||||
err := p.Scan(&key)
|
||||
if err != nil {
|
||||
return LoginErr{"cookie request", err}
|
||||
}
|
||||
cookieContent := c.Cookies[string(key)]
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundLoginCookieResponse,
|
||||
key, pk.OptionEncoder[pk.ByteArray]{
|
||||
Has: cookieContent != nil,
|
||||
Val: pk.ByteArray(cookieContent),
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
return LoginErr{"cookie response", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auth includes an account
|
||||
type Auth struct {
|
||||
Name string
|
||||
UUID string
|
||||
AsTk string
|
||||
}
|
||||
|
||||
func handleEncryptionRequest(conn *net.Conn, c *Client, p pk.Packet) error {
|
||||
// 创建AES对称加密密钥
|
||||
key, encoStream, decoStream := newSymmetricEncryption()
|
||||
|
||||
// Read EncryptionRequest
|
||||
var er encryptionRequest
|
||||
if err := p.Scan(&er); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := loginAuth(c.Auth, key, er) // 向Mojang验证
|
||||
if err != nil {
|
||||
return fmt.Errorf("login fail: %v", err)
|
||||
}
|
||||
|
||||
// 响应加密请求
|
||||
// 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 = conn.WritePacket(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置连接加密
|
||||
conn.SetCipher(encoStream, decoStream)
|
||||
return nil
|
||||
}
|
||||
|
||||
type encryptionRequest struct {
|
||||
ServerID string
|
||||
PublicKey []byte
|
||||
VerifyToken []byte
|
||||
}
|
||||
|
||||
func (e *encryptionRequest) ReadFrom(r io.Reader) (int64, error) {
|
||||
return pk.Tuple{
|
||||
(*pk.String)(&e.ServerID),
|
||||
(*pk.ByteArray)(&e.PublicKey),
|
||||
(*pk.ByteArray)(&e.VerifyToken),
|
||||
}.ReadFrom(r)
|
||||
}
|
||||
|
||||
// authDigest computes a special SHA-1 digest required for Minecraft web
|
||||
// authentication on Premium servers (online-mode=true).
|
||||
// Source: http://wiki.vg/Protocol_Encryption#Server
|
||||
func authDigest(serverID string, sharedSecret, publicKey []byte) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(serverID))
|
||||
h.Write(sharedSecret)
|
||||
h.Write(publicKey)
|
||||
hash := h.Sum(nil)
|
||||
|
||||
// Check for negative hashes
|
||||
negative := (hash[0] & 0x80) == 0x80
|
||||
if negative {
|
||||
hash = twosComplement(hash)
|
||||
}
|
||||
|
||||
// Trim away zeroes
|
||||
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
|
||||
if negative {
|
||||
res = "-" + res
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// little endian
|
||||
func twosComplement(p []byte) []byte {
|
||||
carry := true
|
||||
for i := len(p) - 1; i >= 0; i-- {
|
||||
p[i] = ^p[i]
|
||||
if carry {
|
||||
carry = p[i] == 0xff
|
||||
p[i]++
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
type profile struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type request struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
SelectedProfile profile `json:"selectedProfile"`
|
||||
ServerID string `json:"serverId"`
|
||||
}
|
||||
|
||||
func loginAuth(auth Auth, shareSecret []byte, er encryptionRequest) error {
|
||||
digest := authDigest(er.ServerID, shareSecret, er.PublicKey)
|
||||
|
||||
requestPacket, err := json.Marshal(
|
||||
request{
|
||||
AccessToken: auth.AsTk,
|
||||
SelectedProfile: profile{
|
||||
ID: auth.UUID,
|
||||
Name: auth.Name,
|
||||
},
|
||||
ServerID: digest,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request packet to yggdrasil faile: %v", err)
|
||||
}
|
||||
|
||||
PostRequest, err := http.NewRequest(http.MethodPost, "https://sessionserver.mojang.com/session/minecraft/join",
|
||||
bytes.NewReader(requestPacket))
|
||||
if err != nil {
|
||||
return fmt.Errorf("make request error: %v", err)
|
||||
}
|
||||
PostRequest.Header.Set("User-agent", "go-mc")
|
||||
PostRequest.Header.Set("Connection", "keep-alive")
|
||||
PostRequest.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(PostRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("post fail: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("auth fail: %s", string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AES/CFB8 with random key
|
||||
func newSymmetricEncryption() (key []byte, encoStream, decoStream cipher.Stream) {
|
||||
key = make([]byte, 16)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
decoStream = CFB8.NewCFB8Decrypt(b, key)
|
||||
encoStream = CFB8.NewCFB8Encrypt(b, key)
|
||||
return
|
||||
}
|
||||
|
||||
func genEncryptionKeyResponse(shareSecret, publicKey, verifyToken []byte) (erp pk.Packet, err error) {
|
||||
iPK, err := x509.ParsePKIXPublicKey(publicKey) // Decode Public Key
|
||||
if err != nil {
|
||||
err = fmt.Errorf("decode public key fail: %v", err)
|
||||
return
|
||||
}
|
||||
rsaKey := iPK.(*rsa.PublicKey)
|
||||
cryptPK, err := rsa.EncryptPKCS1v15(rand.Reader, rsaKey, shareSecret)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("encryption share secret fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
verifyT, err := rsa.EncryptPKCS1v15(rand.Reader, rsaKey, verifyToken)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("encryption verfy tokenfail: %v", err)
|
||||
return erp, err
|
||||
}
|
||||
return pk.Marshal(
|
||||
packetid.ServerboundLoginKey,
|
||||
pk.ByteArray(cryptPK),
|
||||
pk.ByteArray(verifyT),
|
||||
), nil
|
||||
}
|
128
bot/mcbot.go
128
bot/mcbot.go
@ -1,128 +0,0 @@
|
||||
// Package bot implements a simple Minecraft client that can join a server
|
||||
// or just ping it for getting information.
|
||||
//
|
||||
// Runnable example could be found at examples/ .
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
mcnet "github.com/Tnze/go-mc/net"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
"github.com/Tnze/go-mc/net/queue"
|
||||
"github.com/Tnze/go-mc/yggdrasil/user"
|
||||
)
|
||||
|
||||
// ProtocolVersion is the protocol version number of minecraft net protocol
|
||||
const (
|
||||
ProtocolVersion = 767
|
||||
DefaultPort = mcnet.DefaultPort
|
||||
)
|
||||
|
||||
type JoinOptions struct {
|
||||
MCDialer mcnet.MCDialer
|
||||
Context context.Context
|
||||
|
||||
// Indicate not to fetch and sending player's PubKey
|
||||
NoPublicKey bool
|
||||
|
||||
// Specify the player PubKey to use.
|
||||
// If nil, it will be obtained from Mojang when joining
|
||||
KeyPair *user.KeyPairResp
|
||||
|
||||
QueueRead queue.Queue[pk.Packet]
|
||||
QueueWrite queue.Queue[pk.Packet]
|
||||
}
|
||||
|
||||
// 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.JoinServerWithOptions(addr, JoinOptions{})
|
||||
}
|
||||
|
||||
// JoinServerWithDialer is similar to JoinServer but using a net.Dialer.
|
||||
func (c *Client) JoinServerWithDialer(dialer *net.Dialer, addr string) (err error) {
|
||||
return c.JoinServerWithOptions(addr, JoinOptions{
|
||||
MCDialer: (*mcnet.Dialer)(dialer),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) JoinServerWithOptions(addr string, options JoinOptions) (err error) {
|
||||
if options.MCDialer == nil {
|
||||
options.MCDialer = &mcnet.DefaultDialer
|
||||
}
|
||||
if options.Context == nil {
|
||||
options.Context = context.Background()
|
||||
}
|
||||
if options.QueueRead == nil {
|
||||
options.QueueRead = queue.NewLinkedQueue[pk.Packet]()
|
||||
}
|
||||
if options.QueueWrite == nil {
|
||||
options.QueueWrite = queue.NewLinkedQueue[pk.Packet]()
|
||||
}
|
||||
return c.join(addr, options)
|
||||
}
|
||||
|
||||
func (c *Client) join(addr string, options JoinOptions) error {
|
||||
const Handshake = 0x00
|
||||
|
||||
// Split Host and Port. The DialMCContext will do this once,
|
||||
// but we need the result for sending handshake packet here.
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
var port uint64
|
||||
if err != nil {
|
||||
var addrErr *net.AddrError
|
||||
const missingPort = "missing port in address"
|
||||
if errors.As(err, &addrErr) && addrErr.Err == missingPort {
|
||||
host = addr
|
||||
port = 25565
|
||||
} else {
|
||||
return LoginErr{"split address", err}
|
||||
}
|
||||
} else {
|
||||
port, err = strconv.ParseUint(portStr, 0, 16)
|
||||
if err != nil {
|
||||
return LoginErr{"parse port", err}
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connection
|
||||
conn, err := options.MCDialer.DialMCContext(options.Context, addr)
|
||||
if err != nil {
|
||||
return LoginErr{"connect server", err}
|
||||
}
|
||||
|
||||
// Handshake
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
Handshake,
|
||||
pk.VarInt(ProtocolVersion), // Protocol version
|
||||
pk.String(host), // Host
|
||||
pk.UnsignedShort(port), // Port
|
||||
pk.VarInt(2),
|
||||
))
|
||||
if err != nil {
|
||||
return LoginErr{"handshake", err}
|
||||
}
|
||||
|
||||
// Login Start
|
||||
if err := c.joinLogin(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Configuration
|
||||
if err := c.joinConfiguration(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Conn = warpConn(conn, options.QueueRead, options.QueueWrite)
|
||||
return nil
|
||||
}
|
||||
|
||||
type DisconnectErr chat.Message
|
||||
|
||||
func (d DisconnectErr) Error() string {
|
||||
return "disconnect because: " + chat.Message(d).String()
|
||||
}
|
221
bot/msg/chat.go
221
bot/msg/chat.go
@ -1,221 +0,0 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/Tnze/go-mc/bot"
|
||||
"github.com/Tnze/go-mc/bot/basic"
|
||||
"github.com/Tnze/go-mc/bot/playerlist"
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
"github.com/Tnze/go-mc/chat/sign"
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
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
|
||||
pl *playerlist.PlayerList
|
||||
events EventsHandler
|
||||
|
||||
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,
|
||||
p: p,
|
||||
pl: pl,
|
||||
events: events,
|
||||
SignatureCache: sign.NewSignatureCache(),
|
||||
}
|
||||
if events.SystemChat != nil {
|
||||
c.Events.AddListener(bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundSystemChat,
|
||||
F: m.handleSystemChat,
|
||||
})
|
||||
}
|
||||
if events.PlayerChatMessage != nil {
|
||||
c.Events.AddListener(bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundPlayerChat,
|
||||
F: m.handlePlayerChat,
|
||||
})
|
||||
}
|
||||
if events.DisguisedChat != nil {
|
||||
c.Events.AddListener(bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundDisguisedChat,
|
||||
F: m.handleDisguisedChat,
|
||||
})
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) handleSystemChat(p pk.Packet) error {
|
||||
var msg chat.Message
|
||||
var overlay pk.Boolean
|
||||
if err := p.Scan(&msg, &overlay); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.events.SystemChat(msg, bool(overlay))
|
||||
}
|
||||
|
||||
func (m *Manager) handlePlayerChat(packet pk.Packet) error {
|
||||
var (
|
||||
sender pk.UUID
|
||||
index pk.VarInt
|
||||
signature pk.Option[sign.Signature, *sign.Signature]
|
||||
body sign.PackedMessageBody
|
||||
unsignedContent pk.Option[chat.Message, *chat.Message]
|
||||
filter sign.FilterMask
|
||||
chatType chat.Type
|
||||
)
|
||||
if err := packet.Scan(&sender, &index, &signature, &body, &unsignedContent, &filter, &chatType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unpackedMsg, err := body.Unpack(&m.SignatureCache)
|
||||
if err != nil {
|
||||
return InvalidChatPacket{err}
|
||||
}
|
||||
senderInfo, ok := m.pl.PlayerInfos[uuid.UUID(sender)]
|
||||
if !ok {
|
||||
return InvalidChatPacket{ErrUnknownPlayer}
|
||||
}
|
||||
ct := m.c.Registries.ChatType.GetByID(chatType.ID)
|
||||
if ct == nil {
|
||||
return InvalidChatPacket{ErrUnknwonChatType}
|
||||
}
|
||||
|
||||
var message sign.Message
|
||||
if senderInfo.ChatSession != nil {
|
||||
message.Prev = sign.Prev{
|
||||
Index: int(index),
|
||||
Sender: uuid.UUID(sender),
|
||||
Session: senderInfo.ChatSession.SessionID,
|
||||
}
|
||||
} else {
|
||||
message.Prev = sign.Prev{
|
||||
Index: 0,
|
||||
Sender: uuid.UUID(sender),
|
||||
Session: uuid.Nil,
|
||||
}
|
||||
}
|
||||
message.Signature = signature.Pointer()
|
||||
message.MessageBody = unpackedMsg
|
||||
message.Unsigned = unsignedContent.Pointer()
|
||||
message.FilterMask = filter
|
||||
|
||||
var validated bool
|
||||
if senderInfo.ChatSession != nil {
|
||||
if !senderInfo.ChatSession.VerifyAndUpdate(&message) {
|
||||
return ErrValidationFailed
|
||||
}
|
||||
validated = true
|
||||
// store signature into signatureCache
|
||||
m.PopOrInsert(signature.Pointer(), message.LastSeen)
|
||||
}
|
||||
|
||||
var content chat.Message
|
||||
if unsignedContent.Has {
|
||||
content = unsignedContent.Val
|
||||
} else {
|
||||
content = chat.Text(body.PlainMsg)
|
||||
}
|
||||
msg := chatType.Decorate(content, &ct.Chat)
|
||||
return m.events.PlayerChatMessage(msg, validated)
|
||||
}
|
||||
|
||||
func (m *Manager) handleDisguisedChat(packet pk.Packet) error {
|
||||
var (
|
||||
message chat.Message
|
||||
chatType chat.Type
|
||||
)
|
||||
if err := packet.Scan(&message, &chatType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ct := m.c.Registries.ChatType.GetByID(chatType.ID)
|
||||
if ct == nil {
|
||||
return InvalidChatPacket{ErrUnknwonChatType}
|
||||
}
|
||||
msg := chatType.Decorate(message, &ct.Chat)
|
||||
|
||||
return m.events.DisguisedChat(msg)
|
||||
}
|
||||
|
||||
// SendMessage send chat message to server.
|
||||
// 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")
|
||||
}
|
||||
|
||||
var salt int64
|
||||
if err := binary.Read(rand.Reader, binary.BigEndian, &salt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := m.c.Conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundChat,
|
||||
pk.String(msg),
|
||||
pk.Long(time.Now().UnixMilli()),
|
||||
pk.Long(salt),
|
||||
pk.Boolean(false), // signature
|
||||
sign.HistoryUpdate{
|
||||
Acknowledged: pk.NewFixedBitSet(20),
|
||||
},
|
||||
))
|
||||
return err
|
||||
}
|
||||
|
||||
// SendMessage send chat message to server.
|
||||
// Doesn't support sending message with signature currently.
|
||||
func (m *Manager) SendCommand(command string) error {
|
||||
if len(command) > 256 {
|
||||
return errors.New("message length greater than 256")
|
||||
}
|
||||
var salt int64
|
||||
if err := binary.Read(rand.Reader, binary.BigEndian, &salt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := m.c.Conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundChatCommand,
|
||||
pk.String(command),
|
||||
pk.Long(time.Now().UnixMilli()),
|
||||
pk.Long(salt),
|
||||
pk.Ary[pk.VarInt]{Ary: []pk.Tuple{}},
|
||||
sign.HistoryUpdate{
|
||||
Acknowledged: pk.NewFixedBitSet(20),
|
||||
},
|
||||
))
|
||||
return err
|
||||
}
|
||||
|
||||
type InvalidChatPacket struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (i InvalidChatPacket) Error() string {
|
||||
if i.err == nil {
|
||||
return "invalid chat packet"
|
||||
}
|
||||
return "invalid chat packet: " + i.err.Error()
|
||||
}
|
||||
|
||||
func (i InvalidChatPacket) Unwrap() error {
|
||||
return i.err
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnknownPlayer = errors.New("unknown player")
|
||||
ErrUnknwonChatType = errors.New("unknown chat type")
|
||||
ErrValidationFailed error = bot.DisconnectErr(chat.TranslateMsg("multiplayer.disconnect.chat_validation_failed"))
|
||||
)
|
@ -1,29 +0,0 @@
|
||||
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 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 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
|
||||
}
|
137
bot/pinglist.go
137
bot/pinglist.go
@ -1,137 +0,0 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
mcnet "github.com/Tnze/go-mc/net"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
// PingAndList check server status and list online player.
|
||||
// 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) ([]byte, time.Duration, error) {
|
||||
conn, err := mcnet.DialMC(addr)
|
||||
if err != nil {
|
||||
return nil, 0, LoginErr{"dial connection", err}
|
||||
}
|
||||
return pingAndList(context.Background(), addr, conn)
|
||||
}
|
||||
|
||||
// PingAndListTimeout is the version of PingAndList with max request time.
|
||||
func PingAndListTimeout(addr string, timeout time.Duration) ([]byte, time.Duration, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return PingAndListContext(ctx, addr)
|
||||
}
|
||||
|
||||
func PingAndListContext(ctx context.Context, addr string) ([]byte, time.Duration, error) {
|
||||
conn, err := mcnet.DefaultDialer.DialMCContext(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return pingAndList(ctx, addr, conn)
|
||||
}
|
||||
|
||||
func pingAndList(ctx context.Context, addr string, conn *mcnet.Conn) (data []byte, delay time.Duration, err error) {
|
||||
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
||||
if err := conn.Socket.SetDeadline(deadline); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() {
|
||||
// Reset deadline
|
||||
if err2 := conn.Socket.SetDeadline(time.Time{}); err2 != nil {
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return
|
||||
}
|
||||
// Map error type
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
err = context.DeadlineExceeded
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Split Host and Port
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
var port uint64
|
||||
if err != nil {
|
||||
var addrErr *net.AddrError
|
||||
const missingPort = "missing port in address"
|
||||
if errors.As(err, &addrErr) && addrErr.Err == missingPort {
|
||||
host, port, err = addr, DefaultPort, nil
|
||||
} else {
|
||||
return nil, 0, LoginErr{"split address", err}
|
||||
}
|
||||
} else {
|
||||
port, err = strconv.ParseUint(portStr, 0, 16)
|
||||
if err != nil {
|
||||
return nil, 0, LoginErr{"parse port", err}
|
||||
}
|
||||
}
|
||||
|
||||
const Handshake = 0x00
|
||||
// 握手
|
||||
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(
|
||||
packetid.ServerboundStatusStatusRequest,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("bot: send list packect fail: %v", err)
|
||||
}
|
||||
|
||||
var p pk.Packet
|
||||
// 服务器返回状态
|
||||
if err := conn.ReadPacket(&p); err != nil {
|
||||
return nil, 0, fmt.Errorf("bot: recv list packect fail: %v", err)
|
||||
}
|
||||
var s pk.String
|
||||
err = p.Scan(&s)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("bot: scan list packect fail: %v", err)
|
||||
}
|
||||
|
||||
// PING
|
||||
startTime := time.Now()
|
||||
err = conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundStatusPingRequest,
|
||||
pk.Long(startTime.Unix()),
|
||||
))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("bot: send ping packect fail: %v", err)
|
||||
}
|
||||
|
||||
if err = conn.ReadPacket(&p); err != nil {
|
||||
return nil, 0, fmt.Errorf("bot: recv pong packect fail: %v", err)
|
||||
}
|
||||
var t pk.Long
|
||||
err = p.Scan(&t)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("bot: scan pong packect fail: %v", err)
|
||||
}
|
||||
if t != pk.Long(startTime.Unix()) {
|
||||
return nil, 0, fmt.Errorf("bot: pong packect no match: %v", err)
|
||||
}
|
||||
|
||||
return []byte(s), time.Since(startTime), err
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/Tnze/go-mc/bot"
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
"github.com/Tnze/go-mc/chat/sign"
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
"github.com/Tnze/go-mc/yggdrasil/user"
|
||||
)
|
||||
|
||||
type PlayerList struct {
|
||||
PlayerInfos map[uuid.UUID]*PlayerInfo
|
||||
}
|
||||
|
||||
func New(c *bot.Client) *PlayerList {
|
||||
pl := PlayerList{
|
||||
PlayerInfos: make(map[uuid.UUID]*PlayerInfo),
|
||||
}
|
||||
c.Events.AddListener(
|
||||
bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundPlayerInfoUpdate,
|
||||
F: pl.handlePlayerInfoUpdatePacket,
|
||||
},
|
||||
bot.PacketHandler{
|
||||
Priority: 64, ID: packetid.ClientboundPlayerInfoRemove,
|
||||
F: pl.handlePlayerInfoRemovePacket,
|
||||
},
|
||||
)
|
||||
return &pl
|
||||
}
|
||||
|
||||
func (pl *PlayerList) handlePlayerInfoUpdatePacket(p pk.Packet) error {
|
||||
r := bytes.NewReader(p.Data)
|
||||
|
||||
action := pk.NewFixedBitSet(6)
|
||||
if _, err := action.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var length pk.VarInt
|
||||
if _, err := length.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < int(length); i++ {
|
||||
var id pk.UUID
|
||||
if _, err := id.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
player, ok := pl.PlayerInfos[uuid.UUID(id)]
|
||||
if !ok { // create new player info if not exist
|
||||
player = new(PlayerInfo)
|
||||
pl.PlayerInfos[uuid.UUID(id)] = player
|
||||
}
|
||||
|
||||
// add player
|
||||
if action.Get(0) {
|
||||
var name pk.String
|
||||
var properties []user.Property
|
||||
if _, err := (pk.Tuple{&name, pk.Array(&properties)}).ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
player.GameProfile = GameProfile{
|
||||
ID: uuid.UUID(id),
|
||||
Name: string(name),
|
||||
Properties: properties,
|
||||
}
|
||||
}
|
||||
// initialize chat
|
||||
if action.Get(1) {
|
||||
var chatSession pk.Option[sign.Session, *sign.Session]
|
||||
if _, err := chatSession.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if chatSession.Has {
|
||||
player.ChatSession = chatSession.Pointer()
|
||||
player.ChatSession.InitValidate()
|
||||
} else {
|
||||
player.ChatSession = nil
|
||||
}
|
||||
}
|
||||
// update gamemode
|
||||
if action.Get(2) {
|
||||
var gamemode pk.VarInt
|
||||
if _, err := gamemode.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
player.Gamemode = int32(gamemode)
|
||||
}
|
||||
// update listed
|
||||
if action.Get(3) {
|
||||
var listed pk.Boolean
|
||||
if _, err := listed.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
player.Listed = bool(listed)
|
||||
}
|
||||
// update latency
|
||||
if action.Get(4) {
|
||||
var latency pk.VarInt
|
||||
if _, err := latency.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
player.Latency = int32(latency)
|
||||
}
|
||||
// display name
|
||||
if action.Get(5) {
|
||||
var displayName pk.Option[chat.Message, *chat.Message]
|
||||
if _, err := displayName.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if displayName.Has {
|
||||
player.DisplayName = &displayName.Val
|
||||
} else {
|
||||
player.DisplayName = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl *PlayerList) handlePlayerInfoRemovePacket(p pk.Packet) error {
|
||||
r := bytes.NewReader(p.Data)
|
||||
var (
|
||||
length pk.VarInt
|
||||
id pk.UUID
|
||||
)
|
||||
if _, err := length.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < int(length); i++ {
|
||||
if _, err := id.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(pl.PlayerInfos, uuid.UUID(id))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PlayerInfo struct {
|
||||
GameProfile
|
||||
ChatSession *sign.Session
|
||||
Gamemode int32
|
||||
Latency int32
|
||||
Listed bool
|
||||
DisplayName *chat.Message
|
||||
}
|
||||
|
||||
type GameProfile struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Properties []user.Property
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package screen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
"github.com/Tnze/go-mc/data/inventory"
|
||||
)
|
||||
|
||||
type Chest struct {
|
||||
Type inventory.InventoryID
|
||||
Title chat.Message
|
||||
Slots []Slot
|
||||
Rows int
|
||||
}
|
||||
|
||||
func (c *Chest) onSetSlot(i int, slot Slot) error {
|
||||
if i < 0 || i >= len(c.Slots) {
|
||||
return errors.New("slot index out of bounds")
|
||||
}
|
||||
c.Slots[i] = slot
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Chest) onClose() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Chest) Container() []Slot {
|
||||
return c.Slots[0 : c.Rows*9]
|
||||
}
|
||||
|
||||
func (c *Chest) Main() []Slot {
|
||||
return c.Slots[c.Rows*9 : c.Rows*9+27]
|
||||
}
|
||||
|
||||
func (c *Chest) Hotbar() []Slot {
|
||||
return c.Slots[c.Rows*9+27 : (c.Rows+4)*9]
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package screen
|
||||
|
||||
import "github.com/Tnze/go-mc/chat"
|
||||
|
||||
type EventsListener struct {
|
||||
Open func(id int, container_type int32, title chat.Message) error
|
||||
SetSlot func(id, index int) error
|
||||
Close func(id int) error
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package screen
|
||||
|
||||
import "errors"
|
||||
|
||||
type Inventory struct {
|
||||
Slots [46]Slot
|
||||
}
|
||||
|
||||
func (inv *Inventory) onClose() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (inv *Inventory) onSetSlot(i int, s Slot) error {
|
||||
if i < 0 || i >= len(inv.Slots) {
|
||||
return errors.New("slot index out of bounds")
|
||||
}
|
||||
inv.Slots[i] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (inv *Inventory) CraftingOutput() *Slot { return &inv.Slots[0] }
|
||||
func (inv *Inventory) CraftingInput() []Slot { return inv.Slots[1 : 1+4] }
|
||||
|
||||
// Armor returns to the armor section of the Inventory.
|
||||
// The length is 4, which are head, chest, legs and feet.
|
||||
func (inv *Inventory) Armor() []Slot { return inv.Slots[5 : 5+4] }
|
||||
func (inv *Inventory) Main() []Slot { return inv.Slots[9 : 9+3*9] }
|
||||
func (inv *Inventory) Hotbar() []Slot { return inv.Slots[36 : 36+9] }
|
||||
func (inv *Inventory) Offhand() *Slot { return &inv.Slots[45] }
|
@ -1,240 +0,0 @@
|
||||
package screen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/Tnze/go-mc/bot"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
c *bot.Client
|
||||
|
||||
Screens map[int]Container
|
||||
Inventory Inventory
|
||||
Cursor Slot
|
||||
events EventsListener
|
||||
// The last received State ID from server
|
||||
stateID int32
|
||||
}
|
||||
|
||||
func NewManager(c *bot.Client, e EventsListener) *Manager {
|
||||
m := &Manager{
|
||||
c: c,
|
||||
Screens: make(map[int]Container),
|
||||
events: e,
|
||||
}
|
||||
m.Screens[0] = &m.Inventory
|
||||
c.Events.AddListener(
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundOpenScreen, F: m.onOpenScreen},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundContainerSetContent, F: m.onSetContentPacket},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundContainerClose, F: m.onCloseScreen},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundContainerSetSlot, F: m.onSetSlot},
|
||||
)
|
||||
return m
|
||||
}
|
||||
|
||||
type ChangedSlots map[int]*Slot
|
||||
|
||||
func (m *Manager) ContainerClick(id int, slot int16, button byte, mode int32, slots ChangedSlots, carried *Slot) error {
|
||||
return m.c.Conn.WritePacket(pk.Marshal(
|
||||
packetid.ServerboundContainerClick,
|
||||
pk.UnsignedByte(id),
|
||||
pk.VarInt(m.stateID),
|
||||
pk.Short(slot),
|
||||
pk.Byte(button),
|
||||
pk.VarInt(mode),
|
||||
slots,
|
||||
carried,
|
||||
))
|
||||
}
|
||||
|
||||
func (c ChangedSlots) WriteTo(w io.Writer) (n int64, err error) {
|
||||
n, err = pk.VarInt(len(c)).WriteTo(w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i, v := range c {
|
||||
n1, err := pk.Short(i).WriteTo(w)
|
||||
if err != nil {
|
||||
return n + n1, err
|
||||
}
|
||||
n2, err := v.WriteTo(w)
|
||||
if err != nil {
|
||||
return n + n1 + n2, err
|
||||
}
|
||||
n += n1 + n2
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Manager) onOpenScreen(p pk.Packet) error {
|
||||
var (
|
||||
ContainerID pk.VarInt
|
||||
Type pk.VarInt
|
||||
Title chat.Message
|
||||
)
|
||||
if err := p.Scan(&ContainerID, &Type, &Title); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
if _, ok := m.Screens[int(ContainerID)]; !ok {
|
||||
TypeInt32 := int32(Type)
|
||||
if TypeInt32 < 6 {
|
||||
Rows := TypeInt32 + 1
|
||||
chest := Chest{
|
||||
Type: TypeInt32,
|
||||
Slots: make([]Slot, 9*Rows),
|
||||
Rows: int(Rows),
|
||||
Title: Title,
|
||||
}
|
||||
m.Screens[int(ContainerID)] = &chest
|
||||
}
|
||||
} else {
|
||||
return errors.New("container id already exists in screens")
|
||||
}
|
||||
if m.events.Open != nil {
|
||||
if err := m.events.Open(int(ContainerID), int32(Type), Title); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) onSetContentPacket(p pk.Packet) error {
|
||||
var (
|
||||
ContainerID pk.UnsignedByte
|
||||
StateID pk.VarInt
|
||||
SlotData []Slot
|
||||
CarriedItem Slot
|
||||
)
|
||||
if err := p.Scan(
|
||||
&ContainerID,
|
||||
&StateID,
|
||||
pk.Array(&SlotData),
|
||||
&CarriedItem,
|
||||
); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
m.stateID = int32(StateID)
|
||||
// copy the slot data to container
|
||||
container, ok := m.Screens[int(ContainerID)]
|
||||
if !ok {
|
||||
return Error{errors.New("setting content of non-exist container")}
|
||||
}
|
||||
for i, v := range SlotData {
|
||||
err := container.onSetSlot(i, v)
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
if m.events.SetSlot != nil {
|
||||
if err := m.events.SetSlot(int(ContainerID), i); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) onCloseScreen(p pk.Packet) error {
|
||||
var ContainerID pk.UnsignedByte
|
||||
if err := p.Scan(&ContainerID); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
if c, ok := m.Screens[int(ContainerID)]; ok {
|
||||
delete(m.Screens, int(ContainerID))
|
||||
if err := c.onClose(); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
if m.events.Close != nil {
|
||||
if err := m.events.Close(int(ContainerID)); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) onSetSlot(p pk.Packet) (err error) {
|
||||
var (
|
||||
ContainerID pk.Byte
|
||||
StateID pk.VarInt
|
||||
SlotID pk.Short
|
||||
SlotData Slot
|
||||
)
|
||||
if err := p.Scan(&ContainerID, &StateID, &SlotID, &SlotData); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
|
||||
m.stateID = int32(StateID)
|
||||
if ContainerID == -1 && SlotID == -1 {
|
||||
m.Cursor = SlotData
|
||||
} else if ContainerID == -2 {
|
||||
err = m.Inventory.onSetSlot(int(SlotID), SlotData)
|
||||
} else if c, ok := m.Screens[int(ContainerID)]; ok {
|
||||
err = c.onSetSlot(int(SlotID), SlotData)
|
||||
}
|
||||
|
||||
if m.events.SetSlot != nil {
|
||||
if err := m.events.SetSlot(int(ContainerID), int(SlotID)); err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return Error{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Slot struct {
|
||||
ID pk.VarInt
|
||||
Count pk.VarInt
|
||||
NBT nbt.RawMessage
|
||||
}
|
||||
|
||||
func (s *Slot) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var present pk.Boolean = s != nil
|
||||
return pk.Tuple{
|
||||
present, pk.Opt{
|
||||
Has: present,
|
||||
Field: pk.Tuple{
|
||||
&s.ID, &s.Count, pk.NBT(&s.NBT),
|
||||
},
|
||||
},
|
||||
}.WriteTo(w)
|
||||
}
|
||||
|
||||
func (s *Slot) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
var componentsAdd, componentsRemove pk.VarInt
|
||||
return pk.Tuple{
|
||||
&s.Count, pk.Opt{
|
||||
Has: func() bool { return s.Count > 0 },
|
||||
Field: pk.Tuple{
|
||||
&s.ID,
|
||||
&componentsAdd,
|
||||
&componentsRemove,
|
||||
// TODO: Components Ignored
|
||||
},
|
||||
},
|
||||
}.ReadFrom(r)
|
||||
}
|
||||
|
||||
type Container interface {
|
||||
onSetSlot(i int, s Slot) error
|
||||
onClose() error
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return "bot/screen: " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Tnze/go-mc/bot"
|
||||
"github.com/Tnze/go-mc/bot/basic"
|
||||
"github.com/Tnze/go-mc/data/packetid"
|
||||
"github.com/Tnze/go-mc/level"
|
||||
pk "github.com/Tnze/go-mc/net/packet"
|
||||
)
|
||||
|
||||
type World struct {
|
||||
c *bot.Client
|
||||
p *basic.Player
|
||||
events EventsListener
|
||||
|
||||
Columns map[level.ChunkPos]*level.Chunk
|
||||
}
|
||||
|
||||
func NewWorld(c *bot.Client, p *basic.Player, events EventsListener) (w *World) {
|
||||
w = &World{
|
||||
c: c, p: p,
|
||||
events: events,
|
||||
Columns: make(map[level.ChunkPos]*level.Chunk),
|
||||
}
|
||||
c.Events.AddListener(
|
||||
bot.PacketHandler{Priority: 64, ID: packetid.ClientboundLogin, F: w.onPlayerSpawn},
|
||||
bot.PacketHandler{Priority: 64, ID: packetid.ClientboundRespawn, F: w.onPlayerSpawn},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundLevelChunkWithLight, F: w.handleLevelChunkWithLightPacket},
|
||||
bot.PacketHandler{Priority: 0, ID: packetid.ClientboundForgetLevelChunk, F: w.handleForgetLevelChunkPacket},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (w *World) onPlayerSpawn(pk.Packet) error {
|
||||
// unload all chunks
|
||||
w.Columns = make(map[level.ChunkPos]*level.Chunk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) handleLevelChunkWithLightPacket(packet pk.Packet) error {
|
||||
var pos level.ChunkPos
|
||||
currentDimType := w.c.Registries.DimensionType.GetByID(w.p.DimensionType)
|
||||
if currentDimType == nil {
|
||||
return fmt.Errorf("dimension type %d not found", w.p.DimensionType)
|
||||
}
|
||||
chunk := level.EmptyChunk(int(currentDimType.Height) / 16)
|
||||
if err := packet.Scan(&pos, chunk); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Columns[pos] = chunk
|
||||
if w.events.LoadChunk != nil {
|
||||
if err := w.events.LoadChunk(pos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) handleForgetLevelChunkPacket(packet pk.Packet) error {
|
||||
var pos level.ChunkPos
|
||||
if err := packet.Scan(&pos); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if w.events.UnloadChunk != nil {
|
||||
err = w.events.UnloadChunk(pos)
|
||||
}
|
||||
delete(w.Columns, pos)
|
||||
return err
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package world
|
||||
|
||||
import "github.com/Tnze/go-mc/level"
|
||||
|
||||
type EventsListener struct {
|
||||
LoadChunk func(pos level.ChunkPos) error
|
||||
UnloadChunk func(pos level.ChunkPos) error
|
||||
}
|
@ -180,6 +180,25 @@ var colors = map[string]string{
|
||||
White: "97",
|
||||
}
|
||||
|
||||
var colorsLegacy = map[string]string{
|
||||
Black: "0",
|
||||
DarkBlue: "1",
|
||||
DarkGreen: "2",
|
||||
DarkAqua: "3",
|
||||
DarkRed: "4",
|
||||
DarkPurple: "5",
|
||||
Gold: "6",
|
||||
Gray: "7",
|
||||
DarkGray: "8",
|
||||
Blue: "9",
|
||||
Green: "a",
|
||||
Aqua: "b",
|
||||
Red: "c",
|
||||
LightPurple: "d",
|
||||
Yellow: "e",
|
||||
White: "f",
|
||||
}
|
||||
|
||||
// translateMap is the translation table.
|
||||
// By default, it's en-us.
|
||||
var translateMap = en_us.Map
|
||||
@ -262,6 +281,51 @@ func (m Message) String() string {
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func (m Message) LegacyString() string {
|
||||
var msg strings.Builder
|
||||
if m.Bold {
|
||||
msg.WriteString("&l")
|
||||
}
|
||||
if m.Italic {
|
||||
msg.WriteString("&o")
|
||||
}
|
||||
if m.UnderLined {
|
||||
msg.WriteString("&n")
|
||||
}
|
||||
if m.StrikeThrough {
|
||||
msg.WriteString("&m")
|
||||
}
|
||||
if m.Color != "" {
|
||||
msg.WriteString("&" + colorsLegacy[m.Color])
|
||||
}
|
||||
|
||||
text := fmtPat.ReplaceAllStringFunc(
|
||||
m.Text,
|
||||
func(str string) string {
|
||||
return "&" + string(str[2])
|
||||
},
|
||||
)
|
||||
msg.WriteString(text)
|
||||
|
||||
// handle translate
|
||||
if m.Translate != "" {
|
||||
args := make([]any, len(m.With))
|
||||
for i, v := range m.With {
|
||||
args[i] = v
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(&msg, translateMap[m.Translate], args...)
|
||||
}
|
||||
|
||||
if m.Extra != nil {
|
||||
for i := range m.Extra {
|
||||
msg.WriteString(m.Extra[i].LegacyString())
|
||||
}
|
||||
}
|
||||
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
var fmtPat = regexp.MustCompile(`(?i)§[\dA-FK-OR]`)
|
||||
|
||||
// TransCtrlSeq will transform control sequences into ANSI code
|
||||
|
@ -1,96 +0,0 @@
|
||||
//go:build generate
|
||||
|
||||
// gen_item.go generates item information.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
)
|
||||
|
||||
const (
|
||||
version = "1.20.3"
|
||||
infoURL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/" + version + "/items.json"
|
||||
// language=gohtml
|
||||
itemTmpl = `// Code generated by gen_item.go DO NOT EDIT.
|
||||
// Package item stores information about items in Minecraft.
|
||||
package item
|
||||
|
||||
// ID describes the numeric ID of an item.
|
||||
type ID uint32
|
||||
|
||||
// Item describes information about a type of item.
|
||||
type Item struct {
|
||||
ID ID
|
||||
DisplayName string
|
||||
Name string
|
||||
StackSize uint
|
||||
}
|
||||
|
||||
var (
|
||||
{{- range .}}
|
||||
{{.CamelName}} = Item{
|
||||
ID: {{.ID}},
|
||||
DisplayName: "{{.DisplayName}}",
|
||||
Name: "{{.Name}}",
|
||||
StackSize: {{.StackSize}},
|
||||
}{{end}}
|
||||
)
|
||||
|
||||
// ByID is an index of minecraft items by their ID.
|
||||
var ByID = map[ID]*Item{ {{range .}}
|
||||
{{.ID}}: &{{.CamelName}},{{end}}
|
||||
}`
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
ID uint32 `json:"id"`
|
||||
CamelName string `json:"-"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Name string `json:"name"`
|
||||
StackSize uint `json:"stackSize"`
|
||||
}
|
||||
|
||||
func downloadInfo() ([]*Item, error) {
|
||||
resp, err := http.Get(infoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data []*Item
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range data {
|
||||
d.CamelName = strcase.ToCamel(d.Name)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
//go:generate go run $GOFILE
|
||||
//go:generate go fmt item.go
|
||||
func main() {
|
||||
fmt.Println("generating item.go")
|
||||
items, err := downloadInfo()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
f, err := os.Create("item.go")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := template.Must(template.New("").Parse(itemTmpl)).Execute(f, items); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
9205
data/item/item.go
9205
data/item/item.go
File diff suppressed because it is too large
Load Diff
@ -329,6 +329,10 @@ func (c *Chunk) PutData(data []byte) error {
|
||||
r := bytes.NewReader(data)
|
||||
for i := range c.Sections {
|
||||
_, err := c.Sections[i].ReadFrom(r)
|
||||
if errors.Is(err, io.EOF) {
|
||||
c.Sections = c.Sections[:i]
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
Reference in New Issue
Block a user