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",
|
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.
|
// translateMap is the translation table.
|
||||||
// By default, it's en-us.
|
// By default, it's en-us.
|
||||||
var translateMap = en_us.Map
|
var translateMap = en_us.Map
|
||||||
@ -262,6 +281,51 @@ func (m Message) String() string {
|
|||||||
return msg.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]`)
|
var fmtPat = regexp.MustCompile(`(?i)§[\dA-FK-OR]`)
|
||||||
|
|
||||||
// TransCtrlSeq will transform control sequences into ANSI code
|
// 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)
|
r := bytes.NewReader(data)
|
||||||
for i := range c.Sections {
|
for i := range c.Sections {
|
||||||
_, err := c.Sections[i].ReadFrom(r)
|
_, err := c.Sections[i].ReadFrom(r)
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
c.Sections = c.Sections[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user