Refactoring package go-mc/bot

This commit is contained in:
Tnze
2021-02-27 01:06:07 +08:00
parent e864580903
commit 3da9321f59
44 changed files with 564 additions and 3715 deletions

32
bot/basic/basic.go Normal file
View File

@ -0,0 +1,32 @@
package basic
import (
"github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
)
type Player struct {
c *bot.Client
Settings Settings
PlayerInfo
WorldInfo
}
func NewPlayer(c *bot.Client, settings Settings) *Player {
b := &Player{c: c, Settings: settings}
c.Events.AddListener(
bot.PacketHandler{Priority: 0, ID: packetid.Login, F: b.handleJoinGamePacket},
bot.PacketHandler{Priority: 0, ID: packetid.KeepAliveClientbound, F: b.handleKeepAlivePacket},
)
return b
}
func (p *Player) Respawn() error {
const PerformRespawn = 0
return p.c.Conn.WritePacket(pk.Marshal(
packetid.ClientCommand,
pk.VarInt(PerformRespawn),
))
}

83
bot/basic/events.go Normal file
View File

@ -0,0 +1,83 @@
package basic
import (
"github.com/google/uuid"
"github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
)
type EventsListener struct {
GameStart func() error
ChatMsg func(c chat.Message, pos byte, uuid uuid.UUID) error
Disconnect func(reason chat.Message) error
HealthChange func(health float32) error
Death func() error
}
func (e EventsListener) Attach(c *bot.Client) {
c.Events.AddListener(
bot.PacketHandler{Priority: 64, ID: packetid.Login, F: e.onJoinGame},
bot.PacketHandler{Priority: 64, ID: packetid.ChatClientbound, F: e.onChatMsg},
bot.PacketHandler{Priority: 64, ID: packetid.Disconnect, F: e.onDisconnect},
bot.PacketHandler{Priority: 64, ID: packetid.UpdateHealth, F: e.onUpdateHealth},
)
}
func (e *EventsListener) onJoinGame(_ pk.Packet) error {
if e.GameStart != nil {
return e.GameStart()
}
return nil
}
func (e *EventsListener) onDisconnect(p pk.Packet) error {
if e.Disconnect != nil {
var reason chat.Message
if err := p.Scan(&reason); err != nil {
return err
}
return e.Disconnect(reason)
}
return nil
}
func (e *EventsListener) onChatMsg(p pk.Packet) error {
if e.ChatMsg != nil {
var msg chat.Message
var pos pk.Byte
var sender pk.UUID
if err := p.Scan(&msg, &pos, &sender); err != nil {
return err
}
return e.ChatMsg(msg, byte(pos), uuid.UUID(sender))
}
return nil
}
func (e *EventsListener) onUpdateHealth(p pk.Packet) error {
if e.ChatMsg != nil {
var health pk.Float
var food pk.VarInt
var foodSaturation pk.Float
if err := p.Scan(&health, &food, &foodSaturation); err != nil {
return err
}
if e.HealthChange != nil {
if err := e.HealthChange(float32(health)); err != nil {
return err
}
}
if e.Death != nil && health <= 0 {
if err := e.Death(); err != nil {
return err
}
}
}
return nil
}

79
bot/basic/info.go Normal file
View File

@ -0,0 +1,79 @@
package basic
import (
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
"unsafe"
)
// WorldInfo content player info in server.
type WorldInfo struct {
DimensionCodec struct {
DimensionType interface{} `nbt:"minecraft:dimension_type"`
WorldgenBiome interface{} `nbt:"minecraft:worldgen/biome"`
}
Dimension interface{}
WorldNames []string // Identifiers for all worlds on the server.
WorldName string // Name of the world being spawned into.
HashedSeed int64 // First 8 bytes of the SHA-256 hash of the world's seed. Used client side for biome noise
MaxPlayers int32 // Was once used by the client to draw the player list, but now is ignored.
ViewDistance int32 // Render distance (2-32).
ReducedDebugInfo bool // If true, a Notchian client shows reduced information on the debug screen. For servers in development, this should almost always be false.
EnableRespawnScreen bool // Set to false when the doImmediateRespawn gamerule is true.
IsDebug bool // True if the world is a debug mode world; debug mode worlds cannot be modified and have predefined blocks.
IsFlat bool // True if the world is a superflat world; flat worlds have different void fog and a horizon at y=0 instead of y=63.
}
type PlayerInfo struct {
EID int32 // The player's Entity ID (EID).
Hardcore bool // Is hardcore
Gamemode byte // Gamemode. 0: Survival, 1: Creative, 2: Adventure, 3: Spectator.
PrevGamemode int8 // Previous Gamemode
}
// ServInfo contains information about the server implementation.
type ServInfo struct {
Brand string
}
func (p *Player) handleJoinGamePacket(packet pk.Packet) error {
var WorldCount pk.VarInt
var WorldNames = []pk.Identifier{}
err := packet.Scan(
(*pk.Int)(&p.EID),
(*pk.Boolean)(&p.Hardcore),
(*pk.UnsignedByte)(&p.Gamemode),
(*pk.Byte)(&p.PrevGamemode),
&WorldCount,
pk.Ary{Len: &WorldCount, Ary: &WorldNames},
&pk.NBT{V: new(interface{})},
&pk.NBT{V: new(interface{})},
(*pk.Identifier)(&p.WorldName),
(*pk.Long)(&p.HashedSeed),
(*pk.VarInt)(&p.MaxPlayers),
(*pk.VarInt)(&p.ViewDistance),
(*pk.Boolean)(&p.ReducedDebugInfo),
(*pk.Boolean)(&p.EnableRespawnScreen),
(*pk.Boolean)(&p.IsDebug),
(*pk.Boolean)(&p.IsFlat),
)
if err != nil {
return err
}
// This line should work "like" the following code but without copy things
// p.WorldNames = make([]string, len(WorldNames))
// for i, v := range WorldNames {
// p.WorldNames[i] = string(v)
// }
p.WorldNames = *(*[]string)(unsafe.Pointer(&WorldNames))
return p.c.Conn.WritePacket(
//PluginMessage packet (serverbound) - sending minecraft brand.
pk.Marshal(
packetid.CustomPayloadServerbound,
pk.Identifier("minecraft:brand"),
pk.String(p.Settings.Brand),
),
)
}

18
bot/basic/keepalive.go Normal file
View File

@ -0,0 +1,18 @@
package basic
import (
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
)
func (p Player) handleKeepAlivePacket(packet pk.Packet) error {
var KeepAliveID pk.Long
if err := packet.Scan(&KeepAliveID); err != nil {
return err
}
// Response
return p.c.Conn.WritePacket(pk.Marshal(
packetid.KeepAliveServerbound,
KeepAliveID,
))
}

View File

@ -1,4 +1,4 @@
package bot
package basic
// Settings of client
type Settings struct {

View File

@ -1,56 +1,24 @@
package bot
import (
"sync"
"time"
"github.com/Tnze/go-mc/bot/path"
"github.com/Tnze/go-mc/bot/phy"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/bot/world/entity/player"
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/net/packet"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
)
// Client is used to access Minecraft server
type Client struct {
conn *net.Conn
Auth
Conn *net.Conn
Auth Auth
player.Player
PlayInfo
ServInfo
abilities PlayerAbilities
settings Settings
Name string
UUID uuid.UUID
Wd world.World //the map data
Inputs path.Inputs
Physics phy.State
lastPosTx time.Time
justTeleported bool
// Delegate allows you push a function to let HandleGame run.
// Do not send at the same goroutine!
Delegate chan func() error
Events eventBroker
closing chan struct{}
inbound chan pk.Packet
wg sync.WaitGroup
Events Events
//TODO: LoginEvents Events
}
func (c *Client) Close() error {
close(c.closing)
err := c.disconnect()
c.wg.Wait()
return err
}
func (c *Client) SendCloseWindow(windowID byte) error {
return c.conn.WritePacket(packet.Marshal(packetid.CloseWindowServerbound, pk.UnsignedByte(windowID)))
return c.Conn.Close()
}
// NewClient init and return a new Client.
@ -62,44 +30,11 @@ func (c *Client) SendCloseWindow(windowID byte) error {
// and load your Name, UUID and AccessToken to client.
func NewClient() *Client {
return &Client{
settings: DefaultSettings,
Auth: Auth{Name: "Steve"},
Delegate: make(chan func() error),
Wd: world.World{
Entities: make(map[int32]*entity.Entity, 8192),
Chunks: make(map[world.ChunkLoc]*world.Chunk, 2048),
},
closing: make(chan struct{}),
inbound: make(chan pk.Packet, 5),
Auth: Auth{Name: "Steve"},
Events: Events{handlers: make(map[int32]*handlerHeap)},
}
}
//PlayInfo content player info in server.
type PlayInfo struct {
Gamemode int //游戏模式
Hardcore bool //是否是极限模式
Dimension int //维度
Difficulty int //难度
ViewDistance int //视距
ReducedDebugInfo bool //减少调试信息
WorldName string //当前世界的名字
IsDebug bool //调试
IsFlat bool //超平坦世界
SpawnPosition Position //主世界出生点
}
// ServInfo contains information about the server implementation.
type ServInfo struct {
Brand string
}
// PlayerAbilities defines what player can do.
type PlayerAbilities struct {
Flags int8
FlyingSpeed float32
FieldofViewModifier float32
}
//Position is a 3D vector.
type Position struct {
X, Y, Z int

View File

@ -1,89 +1,42 @@
package bot
import (
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/bot/world/entity/player"
"github.com/Tnze/go-mc/chat"
"github.com/google/uuid"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
)
type seenPacketFlags uint8
// Valid seenPacketFlags values.
const (
seenJoinGame seenPacketFlags = 1 << iota
seenServerDifficulty
seenPlayerAbilities
seenPlayerInventory
seenUpdateLight
seenChunkData
seenPlayerPositionAndLook
seenSpawnPos
// gameReadyMinPackets are the minimum set of packets that must be seen, for
// the GameReady callback to be invoked.
gameReadyMinPackets = seenJoinGame | seenChunkData | seenUpdateLight |
seenPlayerAbilities | seenPlayerInventory | seenServerDifficulty |
seenPlayerPositionAndLook | seenSpawnPos
)
type eventBroker struct {
seenPackets seenPacketFlags
isReady bool
GameStart func() error
ChatMsg func(msg chat.Message, pos byte, sender uuid.UUID) error
Disconnect func(reason chat.Message) error
HealthChange func() error
Die func() error
SoundPlay func(name string, category int, x, y, z float64, volume, pitch float32) error
PluginMessage func(channel string, data []byte) error
HeldItemChange func(slot int) error
OpenWindow func(pkt ptypes.OpenWindow) error
// ExperienceChange will be called every time player's experience level updates.
// Parameters:
// bar - state of the experience bar from 0.0 to 1.0;
// level - current level;
// total - total amount of experience received from level 0.
ExperienceChange func(bar float32, level int32, total int32) error
WindowsItem func(id byte, slots []entity.Slot) error
WindowsItemChange func(id byte, slotID int, slot entity.Slot) error
WindowConfirmation func(pkt ptypes.ConfirmTransaction) error
// ServerDifficultyChange is called whenever the gamemode of the server changes.
// At time of writing (1.16.3), difficulty values of 0, 1, 2, and 3 correspond
// to peaceful, easy, normal, and hard respectively.
ServerDifficultyChange func(difficulty int) error
// GameReady is called after the client has joined the server and successfully
// received player state. Additionally, the server has begun sending world
// state (such as lighting and chunk information).
//
// Use this callback as a signal as to when your bot should start 'doing'
// things.
GameReady func() error
// PositionChange is called whenever the player position is updated.
PositionChange func(pos player.Pos) error
// ReceivePacket will be called when new packets arrive.
// The default handler will run only if pass == false.
ReceivePacket func(p pk.Packet) (pass bool, err error)
// PrePhysics will be called before a physics tick.
PrePhysics func() error
type Events struct {
handlers map[int32]*handlerHeap
}
func (b *eventBroker) updateSeenPackets(f seenPacketFlags) error {
b.seenPackets |= f
if (^b.seenPackets)&gameReadyMinPackets == 0 && b.GameReady != nil && !b.isReady {
b.isReady = true
return b.GameReady()
func (e *Events) AddListener(listeners ...PacketHandler) {
for _, l := range listeners {
var s *handlerHeap
var ok bool
if s, ok = e.handlers[l.ID]; !ok {
s = &handlerHeap{l}
e.handlers[l.ID] = s
}
s.Push(l)
}
return nil
}
type PacketHandlerFunc func(p pk.Packet) error
type PacketHandler struct {
ID int32
Priority int
F func(p pk.Packet) error
}
// handlerHeap is PriorityQueue<PacketHandlerFunc>
type handlerHeap []PacketHandler
func (h handlerHeap) Len() int { return len(h) }
func (h handlerHeap) Less(i, j int) bool { return h[i].Priority < h[j].Priority }
func (h handlerHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *handlerHeap) Push(x interface{}) { *h = append(*h, x.(PacketHandler)) }
func (h *handlerHeap) Pop() interface{} {
old := *h
n := len(old)
*h = old[0 : n-1]
return old[n-1]
}

View File

@ -1,784 +1,49 @@
package bot
import (
"bytes"
"fmt"
"net/http"
"os"
"time"
"github.com/google/uuid"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/bot/world/entity/player"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/data/soundid"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
)
func (c *Client) updateServerPos(pos player.Pos) (err error) {
prev := c.Player.Pos
c.Player.Pos = pos
switch {
case !prev.LookEqual(pos) && !prev.PosEqual(pos):
err = sendPlayerPositionAndLookPacket(c)
case !prev.PosEqual(pos):
err = sendPlayerPositionPacket(c)
case !prev.LookEqual(pos):
err = sendPlayerLookPacket(c)
case prev.OnGround != pos.OnGround:
err = c.conn.WritePacket(
pk.Marshal(
packetid.Flying,
pk.Boolean(pos.OnGround),
),
)
}
if err != nil {
return err
}
if c.justTeleported || time.Now().Add(-time.Second).After(c.lastPosTx) {
c.justTeleported = false
c.lastPosTx = time.Now()
err = sendPlayerPositionPacket(c)
if err != nil {
return err
}
}
if c.Events.PositionChange != nil && !prev.Equal(pos) {
if err := c.Events.PositionChange(pos); err != nil {
return err
}
}
return nil
}
// HandleGame receive server packet and response them correctly.
// Note that HandleGame will block if you don't receive from Events.
func (c *Client) HandleGame() error {
c.wg.Add(1)
go func() {
defer c.wg.Done()
for {
select {
case <-c.closing:
return
default:
var p pk.Packet
//Read packets
if err := c.conn.ReadPacket(&p); err != nil {
return
}
c.inbound <- p
}
}
}()
cTick := time.NewTicker(time.Second / 10 / 2)
defer cTick.Stop()
var p pk.Packet
for {
select {
case <-c.closing:
return http.ErrServerClosed
case <-cTick.C:
if c.Events.PrePhysics != nil {
if err := c.Events.PrePhysics(); err != nil {
return err
}
}
if err := c.Physics.Tick(c.Inputs, &c.Wd); err != nil {
c.disconnect()
return err
}
c.updateServerPos(c.Physics.Position())
//Read packets
if err := c.Conn.ReadPacket(&p); err != nil {
return err
}
case task := <-c.Delegate:
if err := task(); err != nil {
c.disconnect()
return err
}
case p := <-c.inbound:
//handle packets
disconnect, err := c.handlePacket(p)
if err != nil {
c.disconnect()
return fmt.Errorf("handle packet 0x%X error: %w", p.ID, err)
}
if disconnect {
return nil
}
//handle packets
err := c.handlePacket(p)
if err != nil {
return err
}
}
}
func (c *Client) handlePacket(p pk.Packet) (disconnect bool, err error) {
if c.Events.ReceivePacket != nil {
pass, err := c.Events.ReceivePacket(p)
if err != nil {
return false, err
type PacketHandlerError struct {
ID int32
Err error
}
func (d PacketHandlerError) Error() string {
return fmt.Sprintf("handle packet 0x%X error: %v", d.ID, d.Err)
}
func (d PacketHandlerError) Unwrap() error {
return d.Err
}
func (c *Client) handlePacket(p pk.Packet) (err error) {
if listeners := c.Events.handlers[p.ID]; listeners != nil {
for _, handler := range *listeners {
err = handler.F(p)
if err != nil {
return PacketHandlerError{ID: p.ID, Err: err}
}
}
if pass {
return false, nil
}
}
switch p.ID {
case packetid.Login:
err = handleJoinGamePacket(c, p)
case packetid.CustomPayloadClientbound:
err = handlePluginPacket(c, p)
case packetid.Difficulty:
err = handleServerDifficultyPacket(c, p)
case packetid.SpawnPosition:
err = handleSpawnPositionPacket(c, p)
if err2 := c.Events.updateSeenPackets(seenSpawnPos); err == nil {
err = err2
}
case packetid.AbilitiesClientbound:
err = handlePlayerAbilitiesPacket(c, p)
case packetid.UpdateHealth:
err = handleUpdateHealthPacket(c, p)
case packetid.ChatClientbound:
err = handleChatMessagePacket(c, p)
case packetid.HeldItemSlotClientbound:
err = handleHeldItemPacket(c, p)
case packetid.WindowItems:
err = handleWindowItemsPacket(c, p)
case packetid.OpenWindow:
err = handleOpenWindowPacket(c, p)
case packetid.TransactionClientbound:
err = handleWindowConfirmationPacket(c, p)
case packetid.DeclareRecipes:
// handleDeclareRecipesPacket(g, reader)
case packetid.KeepAliveClientbound:
err = handleKeepAlivePacket(c, p)
case packetid.SpawnEntity:
err = handleSpawnEntityPacket(c, p)
case packetid.NamedEntitySpawn:
err = handleSpawnPlayerPacket(c, p)
case packetid.SpawnEntityLiving:
err = handleSpawnLivingEntityPacket(c, p)
case packetid.Animation:
err = handleEntityAnimationPacket(c, p)
case packetid.EntityStatus:
err = handleEntityStatusPacket(c, p)
case packetid.EntityDestroy:
err = handleDestroyEntitiesPacket(c, p)
case packetid.RelEntityMove:
err = handleEntityPositionPacket(c, p)
case packetid.EntityMoveLook:
err = handleEntityPositionLookPacket(c, p)
case packetid.EntityLook:
err = handleEntityLookPacket(c, p)
case packetid.Entity:
err = handleEntityMovePacket(c, p)
case packetid.UpdateLight:
err = c.Events.updateSeenPackets(seenUpdateLight)
case packetid.MapChunk:
err = handleChunkDataPacket(c, p)
case packetid.BlockChange:
err = handleBlockChangePacket(c, p)
case packetid.MultiBlockChange:
err = handleMultiBlockChangePacket(c, p)
case packetid.UnloadChunk:
err = handleUnloadChunkPacket(c, p)
case packetid.TileEntityData:
err = handleTileEntityDataPacket(c, p)
case packetid.PositionClientbound:
err = handlePlayerPositionAndLookPacket(c, p)
sendPlayerPositionAndLookPacket(c) // to confirm the position
if err2 := c.Events.updateSeenPackets(seenPlayerPositionAndLook); err == nil {
err = err2
}
case packetid.KickDisconnect:
err = handleDisconnectPacket(c, p)
disconnect = true
case packetid.SetSlot:
err = handleSetSlotPacket(c, p)
case packetid.SoundEffect:
err = handleSoundEffect(c, p)
case packetid.NamedSoundEffect:
err = handleNamedSoundEffect(c, p)
case packetid.Experience:
err = handleSetExperience(c, p)
}
return
}
func handleSpawnEntityPacket(c *Client, p pk.Packet) error {
var se ptypes.SpawnEntity
if err := se.Decode(p); err != nil {
return err
}
return c.Wd.OnSpawnEntity(se)
}
func handleSpawnLivingEntityPacket(c *Client, p pk.Packet) error {
var se ptypes.SpawnLivingEntity
if err := se.Decode(p); err != nil {
return err
}
return c.Wd.OnSpawnLivingEntity(se)
}
func handleSpawnPlayerPacket(c *Client, p pk.Packet) error {
var se ptypes.SpawnPlayer
if err := se.Decode(p); err != nil {
return err
}
return c.Wd.OnSpawnPlayer(se)
}
func handleEntityPositionPacket(c *Client, p pk.Packet) error {
var pu ptypes.EntityPosition
if err := pu.Decode(p); err != nil {
return err
}
return c.Wd.OnEntityPosUpdate(pu)
}
func handleEntityPositionLookPacket(c *Client, p pk.Packet) error {
var epr ptypes.EntityPositionLook
if err := epr.Decode(p); err != nil {
return err
}
return c.Wd.OnEntityPosLookUpdate(epr)
}
func handleEntityLookPacket(c *Client, p pk.Packet) error {
var er ptypes.EntityRotation
if err := er.Decode(p); err != nil {
return err
}
return c.Wd.OnEntityLookUpdate(er)
}
func handleEntityMovePacket(c *Client, p pk.Packet) error {
var id pk.VarInt
if err := p.Scan(&id); err != nil {
return err
}
return nil
}
func handleEntityAnimationPacket(c *Client, p pk.Packet) error {
var se ptypes.EntityAnimationClientbound
if err := se.Decode(p); err != nil {
return err
}
return nil
}
func handleEntityStatusPacket(c *Client, p pk.Packet) error {
var (
id pk.Int
status pk.Byte
)
if err := p.Scan(&id, &status); err != nil {
return err
}
return nil
}
func handleDestroyEntitiesPacket(c *Client, p pk.Packet) error {
var count pk.VarInt
var data = pk.Ary{
Len: &count,
Ary: []pk.VarInt{},
}
if err := p.Scan(&count, &data); err != nil {
return err
}
return c.Wd.OnEntityDestroy(data.Ary.([]pk.VarInt))
}
func handleSoundEffect(c *Client, p pk.Packet) error {
var s ptypes.SoundEffect
if err := s.Decode(p); err != nil {
return err
}
if c.Events.SoundPlay != nil {
if soundName, ok := soundid.GetSoundNameByID(soundid.SoundID(s.Sound)); ok {
return c.Events.SoundPlay(
soundName, int(s.Category),
float64(s.X)/8, float64(s.Y)/8, float64(s.Z)/8,
float32(s.Volume), float32(s.Pitch))
}
fmt.Fprintf(os.Stderr, "WARN: Unknown sound name (%v). is data.SoundNames out of date?\n", s.Sound)
}
return nil
}
func handleNamedSoundEffect(c *Client, p pk.Packet) error {
var s ptypes.NamedSoundEffect
if err := s.Decode(p); err != nil {
return err
}
if c.Events.SoundPlay != nil {
return c.Events.SoundPlay(
string(s.Sound), int(s.Category),
float64(s.X)/8, float64(s.Y)/8, float64(s.Z)/8,
float32(s.Volume), float32(s.Pitch))
}
return nil
}
func handleDisconnectPacket(c *Client, p pk.Packet) error {
var reason chat.Message
err := p.Scan(&reason)
if err != nil {
return err
}
if c.Events.Disconnect != nil {
return c.Events.Disconnect(reason)
}
return nil
}
func handleSetSlotPacket(c *Client, p pk.Packet) error {
if c.Events.WindowsItemChange == nil {
return nil
}
var pkt ptypes.SetSlot
if err := pkt.Decode(p); err != nil {
return err
}
return c.Events.WindowsItemChange(byte(pkt.WindowID), int(pkt.Slot), pkt.SlotData)
}
func handleMultiBlockChangePacket(c *Client, p pk.Packet) error {
if !c.settings.ReceiveMap {
return nil
}
var (
loc pk.Long
dontTrustEdges pk.Boolean
sz pk.VarInt
packedBlocks = pk.Ary{
Len: &sz,
Ary: []pk.VarLong{},
}
)
err := p.Scan(&loc, &dontTrustEdges, &sz, &packedBlocks)
if err != nil {
return err
}
x := int((loc >> 42) & ((1 << 22) - 1))
y := int((loc >> 20) & ((1 << 22) - 1))
z := int(loc & ((1 << 20) - 1))
// Apply transform into negative (these numbers are signed)
if x >= 1<<21 {
x -= 1 << 22
}
if z >= 1<<21 {
z -= 1 << 22
}
c.Wd.MultiBlockUpdate(world.ChunkLoc{X: x, Z: z}, y, packedBlocks.Ary.([]pk.VarLong))
return nil
}
func handleBlockChangePacket(c *Client, p pk.Packet) error {
if !c.settings.ReceiveMap {
return nil
}
var (
pos pk.Position
bID pk.VarInt
)
if err := p.Scan(&pos, &bID); err != nil {
return err
}
c.Wd.UnaryBlockUpdate(pos, world.BlockStatus(bID))
return nil
}
func handleChatMessagePacket(c *Client, p pk.Packet) (err error) {
var msg ptypes.ChatMessageClientbound
if err := msg.Decode(p); err != nil {
return err
}
if c.Events.ChatMsg != nil {
return c.Events.ChatMsg(msg.S, byte(msg.Pos), uuid.UUID(msg.Sender))
}
return nil
}
func handleUpdateHealthPacket(c *Client, p pk.Packet) error {
var pkt ptypes.UpdateHealth
if err := pkt.Decode(p); err != nil {
return err
}
c.Health = float32(pkt.Health)
c.Food = int32(pkt.Food)
c.FoodSaturation = float32(pkt.FoodSaturation)
if c.Events.HealthChange != nil {
if err := c.Events.HealthChange(); err != nil {
return err
}
}
if c.Health <= 0 { //player is dead
c.Physics.Run = false
sendPlayerPositionAndLookPacket(c)
if c.Events.Die != nil {
if err := c.Events.Die(); err != nil {
return err
}
}
}
return nil
}
func handleJoinGamePacket(c *Client, p pk.Packet) error {
var pkt ptypes.JoinGame
if err := pkt.Decode(p); err != nil {
return err
}
c.Player.ID = int32(pkt.PlayerEntity)
c.Gamemode = int(pkt.Gamemode & 0x7)
c.Hardcore = pkt.Gamemode&0x8 != 0
c.Dimension = int(pkt.Dimension)
c.WorldName = string(pkt.WorldName)
c.ViewDistance = int(pkt.ViewDistance)
c.ReducedDebugInfo = bool(pkt.RDI)
c.IsDebug = bool(pkt.IsDebug)
c.IsFlat = bool(pkt.IsFlat)
if c.Events.GameStart != nil {
if err := c.Events.GameStart(); err != nil {
return err
}
}
c.conn.WritePacket(
//PluginMessage packet (serverbound) - sending minecraft brand.
pk.Marshal(
packetid.CustomPayloadServerbound,
pk.Identifier("minecraft:brand"),
pk.String(c.settings.Brand),
),
)
if err := c.Events.updateSeenPackets(seenJoinGame); err != nil {
return err
}
return nil
}
func handlePluginPacket(c *Client, p pk.Packet) error {
var msg ptypes.PluginMessage
if err := msg.Decode(p); err != nil {
return err
}
switch msg.Channel {
case "minecraft:brand":
var brandRaw pk.String
if _, err := brandRaw.ReadFrom(bytes.NewReader(msg.Data)); err != nil {
return err
}
c.ServInfo.Brand = string(brandRaw)
}
if c.Events.PluginMessage != nil {
return c.Events.PluginMessage(string(msg.Channel), msg.Data)
}
return nil
}
func handleServerDifficultyPacket(c *Client, p pk.Packet) error {
var difficulty pk.Byte
if err := p.Scan(&difficulty); err != nil {
return err
}
c.Difficulty = int(difficulty)
if c.Events.ServerDifficultyChange != nil {
if err := c.Events.ServerDifficultyChange(c.Difficulty); err != nil {
return err
}
}
return c.Events.updateSeenPackets(seenServerDifficulty)
}
func handleSpawnPositionPacket(c *Client, p pk.Packet) error {
var pos pk.Position
err := p.Scan(&pos)
if err != nil {
return err
}
c.SpawnPosition.X, c.SpawnPosition.Y, c.SpawnPosition.Z =
pos.X, pos.Y, pos.Z
return nil
}
func handlePlayerAbilitiesPacket(c *Client, p pk.Packet) error {
var (
flags pk.Byte
flySpeed pk.Float
viewMod pk.Float
)
err := p.Scan(&flags, &flySpeed, &viewMod)
if err != nil {
return err
}
c.abilities.Flags = int8(flags)
c.abilities.FlyingSpeed = float32(flySpeed)
c.abilities.FieldofViewModifier = float32(viewMod)
c.conn.WritePacket(
//ClientSettings packet (serverbound)
pk.Marshal(
packetid.Settings,
pk.String(c.settings.Locale),
pk.Byte(c.settings.ViewDistance),
pk.VarInt(c.settings.ChatMode),
pk.Boolean(c.settings.ChatColors),
pk.UnsignedByte(c.settings.DisplayedSkinParts),
pk.VarInt(c.settings.MainHand),
),
)
return c.Events.updateSeenPackets(seenPlayerAbilities)
}
func handleHeldItemPacket(c *Client, p pk.Packet) error {
var hi pk.Byte
if err := p.Scan(&hi); err != nil {
return err
}
c.HeldItem = int(hi)
if c.Events.HeldItemChange != nil {
return c.Events.HeldItemChange(c.HeldItem)
}
return nil
}
func handleUnloadChunkPacket(c *Client, p pk.Packet) error {
if !c.settings.ReceiveMap {
return nil
}
var x, z pk.Int
if err := p.Scan(&x, &z); err != nil {
return err
}
c.Wd.UnloadChunk(world.ChunkLoc{X: int(x) >> 4, Z: int(z) >> 4})
return nil
}
func handleChunkDataPacket(c *Client, p pk.Packet) error {
if err := c.Events.updateSeenPackets(seenChunkData); err != nil {
return err
}
if !c.settings.ReceiveMap {
return nil
}
var pkt ptypes.ChunkData
if _, err := pkt.ReadFrom(bytes.NewReader(p.Data)); err != nil {
return err
}
chunk, err := world.DecodeChunkColumn(int32(pkt.PrimaryBitMask), pkt.Data)
if err != nil {
return fmt.Errorf("decode chunk column: %w", err)
}
chunk.TileEntities = make(map[world.TilePosition]entity.BlockEntity, 64)
for _, e := range pkt.BlockEntities {
chunk.TileEntities[world.ToTilePos(e.X, e.Y, e.Z)] = e
}
c.Wd.LoadChunk(int(pkt.X), int(pkt.Z), chunk)
return nil
}
func handleTileEntityDataPacket(c *Client, p pk.Packet) error {
var pkt ptypes.TileEntityData
if _, err := pkt.ReadFrom(bytes.NewReader(p.Data)); err != nil {
return err
}
return c.Wd.TileEntityUpdate(pkt)
}
func handlePlayerPositionAndLookPacket(c *Client, p pk.Packet) error {
var pkt ptypes.PositionAndLookClientbound
if err := pkt.Decode(p); err != nil {
return err
}
pp := c.Player.Pos
if pkt.RelativeX() {
pp.X += float64(pkt.X)
} else {
pp.X = float64(pkt.X)
}
if pkt.RelativeY() {
pp.Y += float64(pkt.Y)
} else {
pp.Y = float64(pkt.Y)
}
if pkt.RelativeZ() {
pp.Z += float64(pkt.Z)
} else {
pp.Z = float64(pkt.Z)
}
if pkt.RelativeYaw() {
pp.Yaw += float32(pkt.Yaw)
} else {
pp.Yaw = float32(pkt.Yaw)
}
if pkt.RelativePitch() {
pp.Pitch += float32(pkt.Pitch)
} else {
pp.Pitch = float32(pkt.Pitch)
}
if err := c.Physics.ServerPositionUpdate(pp, &c.Wd); err != nil {
return err
}
c.Player.Pos = pp
c.justTeleported = true
if c.Events.PositionChange != nil {
if err := c.Events.PositionChange(pp); err != nil {
return err
}
}
//Confirm
return c.conn.WritePacket(pk.Marshal(
packetid.TeleportConfirm,
pk.VarInt(pkt.TeleportID),
))
}
func handleKeepAlivePacket(c *Client, p pk.Packet) error {
var KeepAliveID pk.Long
if err := p.Scan(&KeepAliveID); err != nil {
return err
}
//Response
return c.conn.WritePacket(pk.Marshal(
packetid.KeepAliveServerbound,
KeepAliveID,
))
}
func handleWindowItemsPacket(c *Client, p pk.Packet) error {
var pkt ptypes.WindowItems
if _, err := pkt.ReadFrom(bytes.NewReader(p.Data)); err != nil {
return err
}
if pkt.WindowID == 0 { // Window ID 0 is the players' inventory.
if err := c.Events.updateSeenPackets(seenPlayerInventory); err != nil {
return err
}
}
if c.Events.WindowsItem != nil {
return c.Events.WindowsItem(byte(pkt.WindowID), pkt.Slots)
}
return nil
}
func handleOpenWindowPacket(c *Client, p pk.Packet) error {
var pkt ptypes.OpenWindow
if err := pkt.Decode(p); err != nil {
return err
}
if c.Events.OpenWindow != nil {
return c.Events.OpenWindow(pkt)
}
return nil
}
func handleWindowConfirmationPacket(c *Client, p pk.Packet) error {
var pkt ptypes.ConfirmTransaction
if err := pkt.Decode(p); err != nil {
return err
}
if c.Events.WindowConfirmation != nil {
return c.Events.WindowConfirmation(pkt)
}
return nil
}
func handleSetExperience(c *Client, p pk.Packet) (err error) {
var (
bar pk.Float
level pk.VarInt
total pk.VarInt
)
if err := p.Scan(&bar, &level, &total); err != nil {
return err
}
c.Level = int32(level)
if c.Events.ExperienceChange != nil {
return c.Events.ExperienceChange(float32(bar), int32(level), int32(total))
}
return nil
}
func sendPlayerPositionAndLookPacket(c *Client) error {
return c.conn.WritePacket(ptypes.PositionAndLookServerbound{
X: pk.Double(c.Pos.X),
Y: pk.Double(c.Pos.Y),
Z: pk.Double(c.Pos.Z),
Yaw: pk.Float(c.Pos.Yaw),
Pitch: pk.Float(c.Pos.Pitch),
OnGround: pk.Boolean(c.Pos.OnGround),
}.Encode())
}
func sendPlayerPositionPacket(c *Client) error {
return c.conn.WritePacket(ptypes.Position{
X: pk.Double(c.Pos.X),
Y: pk.Double(c.Pos.Y),
Z: pk.Double(c.Pos.Z),
OnGround: pk.Boolean(c.Pos.OnGround),
}.Encode())
}
func sendPlayerLookPacket(c *Client) error {
return c.conn.WritePacket(ptypes.Look{
Yaw: pk.Float(c.Pos.Yaw),
Pitch: pk.Float(c.Pos.Pitch),
OnGround: pk.Boolean(c.Pos.OnGround),
}.Encode())
}

View File

@ -25,34 +25,35 @@ type Auth struct {
AsTk string
}
// 加密请求
func handleEncryptionRequest(c *Client, pack pk.Packet) error {
//创建AES对称加密密钥
func handleEncryptionRequest(c *Client, p pk.Packet) error {
// 创建AES对称加密密钥
key, encoStream, decoStream := newSymmetricEncryption()
//解析EncryptionRequest
// Read EncryptionRequest
var er encryptionRequest
if err := pack.Scan(&er); err != nil {
if err := p.Scan(&er); err != nil {
return err
}
err := loginAuth(c.AsTk, c.Name, c.Auth.UUID, key, er) //向Mojang验证
err := loginAuth(c.Auth, key, er) //向Mojang验证
if err != nil {
return fmt.Errorf("login fail: %v", err)
}
// 响应加密请求
var p pk.Packet // Encryption Key Response
// Write Encryption Key Response
p, err = genEncryptionKeyResponse(key, er.PublicKey, er.VerifyToken)
if err != nil {
return fmt.Errorf("gen encryption key response fail: %v", err)
}
err = c.conn.WritePacket(p)
err = c.Conn.WritePacket(p)
if err != nil {
return err
}
// 设置连接加密
c.conn.SetCipher(encoStream, decoStream)
c.Conn.SetCipher(encoStream, decoStream)
return nil
}
@ -122,16 +123,16 @@ type request struct {
ServerID string `json:"serverId"`
}
func loginAuth(AsTk, name, UUID string, shareSecret []byte, er encryptionRequest) error {
func loginAuth(auth Auth, shareSecret []byte, er encryptionRequest) error {
digest := authDigest(er.ServerID, shareSecret, er.PublicKey)
client := http.Client{}
requestPacket, err := json.Marshal(
request{
AccessToken: AsTk,
AccessToken: auth.AsTk,
SelectedProfile: profile{
ID: UUID,
Name: name,
ID: auth.UUID,
Name: auth.Name,
},
ServerID: digest,
},
@ -155,7 +156,7 @@ func loginAuth(AsTk, name, UUID string, shareSecret []byte, er encryptionRequest
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.Status != "204 No Content" {
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("auth fail: %s", string(body))
}
return nil

View File

@ -6,10 +6,10 @@ package bot
import (
"context"
"fmt"
"net"
"strconv"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
mcnet "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet"
@ -22,9 +22,15 @@ const DefaultPort = 25565
// JoinServer connect a Minecraft server for playing the game.
// Using roughly the same way to parse address as minecraft.
func (c *Client) JoinServer(addr string) (err error) {
return c.JoinServerWithDialer(&net.Dialer{}, addr)
return c.join(&net.Dialer{}, addr)
}
// JoinServerWithDialer is similar to JoinServer but using a Dialer.
func (c *Client) JoinServerWithDialer(d *net.Dialer, addr string) (err error) {
return c.join(d, addr)
}
// parseAddress will lookup SRV records for the address
func parseAddress(r *net.Resolver, addr string) (string, error) {
const missingPort = "missing port in address"
var port uint16
@ -49,113 +55,110 @@ func parseAddress(r *net.Resolver, addr string) (string, error) {
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)), nil
}
// JoinServerWithDialer is similar to JoinServer but using a Dialer.
func (c *Client) JoinServerWithDialer(d *net.Dialer, addr string) (err error) {
addr, err = parseAddress(d.Resolver, addr)
func (c *Client) join(d *net.Dialer, addr string) error {
const Handshake = 0x00
addrSrv, err := parseAddress(d.Resolver, addr)
if err != nil {
return fmt.Errorf("parse address error: %w", err)
return LoginErr{"resolved address", err}
}
return c.join(d, addr)
}
func (c *Client) join(d *net.Dialer, addr string) (err error) {
conn, err := d.Dial("tcp", addr)
// Split Host and Port
host, portStr, err := net.SplitHostPort(addrSrv)
if err != nil {
err = fmt.Errorf("bot: connect server fail: %v", err)
return err
}
//Set Conn
c.conn = mcnet.WrapConn(conn)
//Get Host and Port
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
err = fmt.Errorf("bot: connect server fail: %v", err)
return err
return LoginErr{"split address", err}
}
port, err := strconv.Atoi(portStr)
if err != nil {
err = fmt.Errorf("bot: connect server fail: %v", err)
return err
return LoginErr{"parse port", err}
}
//Handshake
err = c.conn.WritePacket(
//Handshake Packet
pk.Marshal(
0x00, //Handshake packet ID
pk.VarInt(ProtocolVersion), //Protocol version
pk.String(host), //Server's address
pk.UnsignedShort(port),
pk.Byte(2),
))
// Dial connection
c.Conn, err = mcnet.DialMC(addrSrv)
if err != nil {
err = fmt.Errorf("bot: send handshake packect fail: %v", err)
return
return LoginErr{"connect server", err}
}
//Login
err = c.conn.WritePacket(
//LoginStart Packet
pk.Marshal(0, pk.String(c.Name)))
// Handshake
err = c.Conn.WritePacket(pk.Marshal(
Handshake,
pk.VarInt(ProtocolVersion), // Protocol version
pk.String(host), // Host
pk.UnsignedShort(port), // Port
pk.Byte(2),
))
if err != nil {
err = fmt.Errorf("bot: send login start packect fail: %v", err)
return
return LoginErr{"handshake", err}
}
// Login Start
err = c.Conn.WritePacket(pk.Marshal(
packetid.LoginStart,
pk.String(c.Auth.Name),
))
if err != nil {
return LoginErr{"login start", err}
}
for {
//Recive Packet
var pack pk.Packet
if err = c.conn.ReadPacket(&pack); err != nil {
err = fmt.Errorf("bot: recv packet for Login fail: %v", err)
return
//Receive Packet
var p pk.Packet
if err = c.Conn.ReadPacket(&p); err != nil {
return LoginErr{"receive packet", err}
}
//Handle Packet
switch pack.ID {
case 0x00: //Disconnect
var reason pk.String
err = pack.Scan(&reason)
switch p.ID {
case packetid.Disconnect: //Disconnect
var reason chat.Message
err = p.Scan(&reason)
if err != nil {
err = fmt.Errorf("bot: read Disconnect message fail: %v", err)
} else {
err = fmt.Errorf("bot: connect disconnected by server: %s", reason)
return LoginErr{"disconnect", err}
}
return
case 0x01: //Encryption Request
if err := handleEncryptionRequest(c, pack); err != nil {
return fmt.Errorf("bot: encryption fail: %v", err)
return LoginErr{"disconnect", DisconnectErr(reason)}
case packetid.EncryptionBeginClientbound: //Encryption Request
if err := handleEncryptionRequest(c, p); err != nil {
return LoginErr{"encryption", err}
}
case packetid.Success: //Login Success
err := p.Scan(
(*pk.UUID)(&c.UUID),
(*pk.String)(&c.Name),
)
if err != nil {
return LoginErr{"login success", err}
}
case 0x02: //Login Success
// uuid, l := pk.UnpackString(pack.Data)
// name, _ := unpackString(pack.Data[l:])
return nil
case 0x03: //Set Compression
case packetid.Compress: //Set Compression
var threshold pk.VarInt
if err := pack.Scan(&threshold); err != nil {
return fmt.Errorf("bot: set compression fail: %v", err)
}
c.conn.SetThreshold(int(threshold))
case 0x04: //Login Plugin Request
if err := handlePluginPacket(c, pack); err != nil {
return fmt.Errorf("bot: handle plugin packet fail: %v", err)
if err := p.Scan(&threshold); err != nil {
return LoginErr{"compression", err}
}
c.Conn.SetThreshold(int(threshold))
case packetid.LoginPluginRequest: //Login Plugin Request
// TODO: Handle login plugin request
}
}
}
// Conn return the MCConn of the Client.
// Only used when you want to handle the packets by yourself
func (c *Client) Conn() *mcnet.Conn {
return c.conn
type LoginErr struct {
Stage string
Err error
}
// SendMessage sends a chat message.
func (c *Client) SendMessage(msg string) error {
return c.conn.WritePacket(
pk.Marshal(
packetid.ChatServerbound,
pk.String(msg),
),
)
func (l LoginErr) Error() string {
return "bot: " + l.Stage + " error: " + l.Err.Error()
}
func (l LoginErr) Unwrap() error {
return l.Err
}
type DisconnectErr chat.Message
func (d DisconnectErr) Error() string {
return "disconnect because: " + chat.Message(d).String()
}

View File

@ -1,190 +0,0 @@
package bot
import (
"errors"
"strconv"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
)
// SwingArm swing player's arm.
// hand could be one of 0: main hand, 1: off hand.
// It's just animation.
func (c *Client) SwingArm(hand int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.ArmAnimation,
pk.VarInt(hand),
))
}
// Respawn the player when it was dead.
func (c *Client) Respawn() error {
return c.conn.WritePacket(pk.Marshal(
packetid.ClientCommand,
pk.VarInt(0),
))
}
// UseItem use the item player handing.
// hand could be one of 0: main hand, 1: off hand
func (c *Client) UseItem(hand int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.UseItem,
pk.VarInt(hand),
))
}
// UseEntity used by player to right-clicks another entity.
// hand could be one of 0: main hand, 1: off hand.
// A Notchian server only accepts this packet if
// the entity being attacked/used is visible without obstruction
// and within a 4-unit radius of the player's position.
func (c *Client) UseEntity(entityID int32, hand int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.UseEntity,
pk.VarInt(entityID),
pk.VarInt(0),
pk.VarInt(hand),
))
}
// AttackEntity used by player to left-clicks another entity.
// The attack version of UseEntity. Has the same limit.
func (c *Client) AttackEntity(entityID int32, hand int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.UseEntity,
pk.VarInt(entityID),
pk.VarInt(1),
))
}
// UseEntityAt is a variety of UseEntity with target location
func (c *Client) UseEntityAt(entityID int32, x, y, z float32, hand int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.UseEntity,
pk.VarInt(entityID),
pk.VarInt(2),
pk.Float(x), pk.Float(y), pk.Float(z),
pk.VarInt(hand),
))
}
// Chat send chat as chat message or command at textbox.
func (c *Client) Chat(msg string) error {
if len(msg) > 256 {
return errors.New("message too long")
}
return c.conn.WritePacket(pk.Marshal(
packetid.ChatServerbound,
pk.String(msg),
))
}
// PluginMessage is used by mods and plugins to send their data.
func (c *Client) PluginMessage(channel string, msg []byte) error {
return c.conn.WritePacket((&ptypes.PluginMessage{
Channel: pk.Identifier(channel),
Data: ptypes.PluginData(msg),
}).Encode())
}
// UseBlock is used to place or use a block.
// hand is the hand from which the block is placed; 0: main hand, 1: off hand.
// face is the face on which the block is placed.
//
// Cursor position is the position of the crosshair on the block:
// cursorX, from 0 to 1 increasing from west to east;
// cursorY, from 0 to 1 increasing from bottom to top;
// cursorZ, from 0 to 1 increasing from north to south.
//
// insideBlock is true when the player's head is inside of a block's collision.
func (c *Client) UseBlock(hand, locX, locY, locZ, face int, cursorX, cursorY, cursorZ float32, insideBlock bool) error {
return c.conn.WritePacket(pk.Marshal(
packetid.UseItem,
pk.VarInt(hand),
pk.Position{X: locX, Y: locY, Z: locZ},
pk.VarInt(face),
pk.Float(cursorX), pk.Float(cursorY), pk.Float(cursorZ),
pk.Boolean(insideBlock),
))
}
// SelectItem used to change the slot selection in hotbar.
// slot should from 0 to 8
func (c *Client) SelectItem(slot int) error {
if slot < 0 || slot > 8 {
return errors.New("invalid slot: " + strconv.Itoa(slot))
}
return c.conn.WritePacket(pk.Marshal(
packetid.HeldItemSlotServerbound,
pk.Short(slot),
))
}
// PickItem used to swap out an empty space on the hotbar with the item in the given inventory slot.
// The Notchain client uses this for pick block functionality (middle click) to retrieve items from the inventory.
//
// The server will first search the player's hotbar for an empty slot,
// starting from the current slot and looping around to the slot before it.
// If there are no empty slots, it will start a second search from the
// current slot and find the first slot that does not contain an enchanted item.
// If there still are no slots that meet that criteria, then the server will
// use the currently selected slot. After finding the appropriate slot,
// the server swaps the items and then change player's selected slot (cause the HeldItemChange event).
func (c *Client) PickItem(slot int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.PickItem,
pk.VarInt(slot),
))
}
func (c *Client) playerAction(status, locX, locY, locZ, face int) error {
return c.conn.WritePacket(pk.Marshal(
packetid.BlockDig,
pk.VarInt(status),
pk.Position{X: locX, Y: locY, Z: locZ},
pk.Byte(face),
))
}
// Dig used to start, end or cancel a digging
// status is 0 for start digging, 1 for cancel and 2 if client think it done.
// To digging a block without cancel, use status 0 and 2 once each.
func (c *Client) Dig(status, locX, locY, locZ, face int) error {
return c.playerAction(status, locX, locY, locZ, face)
}
// DropItemStack drop the entire selected stack
func (c *Client) DropItemStack() error {
return c.playerAction(3, 0, 0, 0, 0)
}
// DropItem drop one item in selected stack
func (c *Client) DropItem() error {
return c.playerAction(4, 0, 0, 0, 0)
}
// UseItemEnd used to finish UseItem, like eating food, pulling back bows.
func (c *Client) UseItemEnd() error {
return c.playerAction(5, 0, 0, 0, 0)
}
// SwapItem used to swap the items in hands.
func (c *Client) SwapItem() error {
return c.playerAction(6, 0, 0, 0, 0)
}
// Disconnect disconnect the server.
// Server will close the connection.
func (c *Client) disconnect() error {
return c.conn.Close()
}
// SendPacket send the packet to server.
func (c *Client) SendPacket(packet pk.Packet) error {
return c.conn.WritePacket(packet)
}

View File

@ -1,179 +0,0 @@
package path
import (
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
)
var (
safeStepBlocks = make(map[world.BlockStatus]struct{}, 1024)
stepBlocks = []block.Block{
block.Stone,
block.Granite,
block.PolishedGranite,
block.Diorite,
block.PolishedDiorite,
block.Andesite,
block.PolishedAndesite,
block.GrassBlock,
block.GrassPath,
block.Dirt,
block.CoarseDirt,
block.Cobblestone,
block.OakPlanks,
block.SprucePlanks,
block.BirchPlanks,
block.JunglePlanks,
block.AcaciaPlanks,
block.DarkOakPlanks,
block.Bedrock,
block.GoldOre,
block.IronOre,
block.CoalOre,
block.Glass,
block.LapisOre,
block.Sandstone,
block.RedstoneOre,
block.OakStairs,
block.AcaciaStairs,
block.DarkOakStairs,
block.RedSandstoneStairs,
block.PolishedGraniteStairs,
block.SmoothRedSandstoneStairs,
block.MossyStoneBrickStairs,
block.PolishedDioriteStairs,
block.MossyCobblestoneStairs,
block.EndStoneBrickStairs,
block.StoneStairs,
block.SmoothSandstoneStairs,
block.SmoothQuartzStairs,
block.GraniteStairs,
block.AndesiteStairs,
block.RedNetherBrickStairs,
block.PolishedAndesiteStairs,
block.OakSlab,
block.AcaciaSlab,
block.DarkOakSlab,
block.RedSandstoneSlab,
block.PolishedGraniteSlab,
block.SmoothRedSandstoneSlab,
block.MossyStoneBrickSlab,
block.PolishedDioriteSlab,
block.MossyCobblestoneSlab,
block.EndStoneBrickSlab,
block.StoneSlab,
block.SmoothSandstoneSlab,
block.SmoothQuartzSlab,
block.GraniteSlab,
block.AndesiteSlab,
block.RedNetherBrickSlab,
block.PolishedAndesiteSlab,
}
slabs = map[block.ID]struct{}{
block.OakSlab.ID: struct{}{},
block.AcaciaSlab.ID: struct{}{},
block.DarkOakSlab.ID: struct{}{},
block.RedSandstoneSlab.ID: struct{}{},
block.PolishedGraniteSlab.ID: struct{}{},
block.SmoothRedSandstoneSlab.ID: struct{}{},
block.MossyStoneBrickSlab.ID: struct{}{},
block.PolishedDioriteSlab.ID: struct{}{},
block.MossyCobblestoneSlab.ID: struct{}{},
block.EndStoneBrickSlab.ID: struct{}{},
block.StoneSlab.ID: struct{}{},
block.SmoothSandstoneSlab.ID: struct{}{},
block.SmoothQuartzSlab.ID: struct{}{},
block.GraniteSlab.ID: struct{}{},
block.AndesiteSlab.ID: struct{}{},
block.RedNetherBrickSlab.ID: struct{}{},
block.PolishedAndesiteSlab.ID: struct{}{},
}
stairs = map[block.ID]struct{}{
block.OakStairs.ID: struct{}{},
block.AcaciaStairs.ID: struct{}{},
block.DarkOakStairs.ID: struct{}{},
block.RedSandstoneStairs.ID: struct{}{},
block.PolishedGraniteStairs.ID: struct{}{},
block.SmoothRedSandstoneStairs.ID: struct{}{},
block.MossyStoneBrickStairs.ID: struct{}{},
block.PolishedDioriteStairs.ID: struct{}{},
block.MossyCobblestoneStairs.ID: struct{}{},
block.EndStoneBrickStairs.ID: struct{}{},
block.StoneStairs.ID: struct{}{},
block.SmoothSandstoneStairs.ID: struct{}{},
block.SmoothQuartzStairs.ID: struct{}{},
block.GraniteStairs.ID: struct{}{},
block.AndesiteStairs.ID: struct{}{},
block.RedNetherBrickStairs.ID: struct{}{},
block.PolishedAndesiteStairs.ID: struct{}{},
}
safeWalkBlocks = make(map[world.BlockStatus]struct{}, 128)
walkBlocks = []block.Block{
block.Air,
block.CaveAir,
block.Grass,
block.Torch,
block.OakSign,
block.SpruceSign,
block.BirchSign,
block.AcaciaSign,
block.JungleSign,
block.DarkOakSign,
block.OakWallSign,
block.SpruceWallSign,
block.BirchWallSign,
block.AcaciaWallSign,
block.JungleWallSign,
block.DarkOakWallSign,
block.Cornflower,
block.TallGrass,
}
additionalCostBlocks = map[*block.Block]int{
&block.Rail: 120,
&block.PoweredRail: 200,
}
)
func init() {
for _, b := range stepBlocks {
if b.MinStateID == b.MaxStateID {
safeStepBlocks[world.BlockStatus(b.MinStateID)] = struct{}{}
} else {
for i := b.MinStateID; i <= b.MaxStateID; i++ {
safeStepBlocks[world.BlockStatus(i)] = struct{}{}
}
}
}
for _, b := range walkBlocks {
if b.MinStateID == b.MaxStateID {
safeWalkBlocks[world.BlockStatus(b.MinStateID)] = struct{}{}
} else {
for i := b.MinStateID; i <= b.MaxStateID; i++ {
safeWalkBlocks[world.BlockStatus(i)] = struct{}{}
}
}
}
}
func SteppableBlock(bID world.BlockStatus) bool {
_, ok := safeStepBlocks[bID]
return ok
}
func AirLikeBlock(bID world.BlockStatus) bool {
_, ok := safeWalkBlocks[bID]
return ok
}
func IsLadder(bID world.BlockStatus) bool {
return uint32(bID) >= block.Ladder.MinStateID && uint32(bID) <= block.Ladder.MaxStateID
}
func IsSlab(bID world.BlockStatus) bool {
_, isSlab := slabs[block.StateID[uint32(bID)]]
return isSlab
}

View File

@ -1,10 +0,0 @@
package path
// Inputs describes the desired movements of the player.
type Inputs struct {
Yaw, Pitch float64
ThrottleX, ThrottleZ float64
Jump bool
}

View File

@ -1,362 +0,0 @@
package path
import (
"fmt"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
)
// Cardinal directions.
type Direction uint8
func (d Direction) Offset() (x, y, z int) {
switch d {
case North:
return 0, 0, -1
case South:
return 0, 0, 1
case East:
return 1, 0, 0
case West:
return -1, 0, 0
}
panic(fmt.Sprintf("unknown direction value: %v", d))
}
func (d Direction) Offset2x() (x, y, z int) {
x, y, z = d.Offset()
return x + x, y + y, z + z
}
func (d Direction) String() string {
switch d {
case North:
return "north"
case South:
return "south"
case East:
return "east"
case West:
return "west"
}
return fmt.Sprintf("Direction?<%d>", int(d))
}
// Valid direction values.
const (
North Direction = iota
South
West
East
)
func LadderDirection(bStateID world.BlockStatus) Direction {
return Direction(((uint32(bStateID) - block.Ladder.MinStateID) & 0xE) >> 1)
}
func ChestDirection(bStateID world.BlockStatus) Direction {
return Direction(((uint32(bStateID) - block.Chest.MinStateID) / 6) & 0x3)
}
func StairsDirection(bStateID world.BlockStatus) Direction {
b := block.StateID[uint32(bStateID)]
return Direction(((uint32(bStateID) - block.ByID[b].MinStateID) / 20) & 0x3)
}
func SlabIsBottom(bStateID world.BlockStatus) bool {
b := block.StateID[uint32(bStateID)]
return ((uint32(bStateID)-block.ByID[b].MinStateID)&0xE)>>1 == 1
}
// Movement represents a single type of movement in a path.
type Movement uint8
var allMovements = []Movement{TraverseNorth, TraverseSouth, TraverseEast, TraverseWest,
TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast,
DropNorth, DropSouth, DropEast, DropWest,
Drop2North, Drop2South, Drop2East, Drop2West,
AscendNorth, AscendSouth, AscendEast, AscendWest,
DescendLadder, DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest,
AscendLadder,
JumpCrossNorth, JumpCrossSouth, JumpCrossEast, JumpCrossWest,
}
// Valid movement values.
const (
Waypoint Movement = iota
TraverseNorth
TraverseSouth
TraverseEast
TraverseWest
TraverseNorthEast
TraverseNorthWest
TraverseSouthEast
TraverseSouthWest
DropNorth
DropSouth
DropEast
DropWest
Drop2North
Drop2South
Drop2East
Drop2West
AscendNorth
AscendSouth
AscendEast
AscendWest
DescendLadder
DescendLadderNorth
DescendLadderSouth
DescendLadderEast
DescendLadderWest
AscendLadder
JumpCrossEast
JumpCrossWest
JumpCrossNorth
JumpCrossSouth
)
func (m Movement) Possible(nav *Nav, x, y, z int, from V3, previous Movement) bool {
switch m {
case Waypoint, TraverseNorth, TraverseSouth, TraverseEast, TraverseWest:
if !SteppableBlock(nav.World.GetBlockStatus(x, y, z)) {
return false
}
b1, b2 := nav.World.GetBlockStatus(x, y+1, z), nav.World.GetBlockStatus(x, y+2, z)
u1, u2 := AirLikeBlock(b1) || IsLadder(b1), AirLikeBlock(b2) || IsLadder(b2)
return u1 && u2
case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast:
if !SteppableBlock(nav.World.GetBlockStatus(x, y, z)) {
return false
}
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) {
return false
}
if !AirLikeBlock(nav.World.GetBlockStatus(from.X, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(from.X, y+2, z)) {
return false
}
return AirLikeBlock(nav.World.GetBlockStatus(x, y+1, from.Z)) && AirLikeBlock(nav.World.GetBlockStatus(x, y+2, from.Z))
case Drop2North, Drop2South, Drop2East, Drop2West:
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+4, z)) {
return false
}
fallthrough
case DropNorth, DropSouth, DropEast, DropWest:
for amt := 0; amt < 3; amt++ {
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+amt+1, z)) {
return false
}
}
return SteppableBlock(nav.World.GetBlockStatus(x, y, z))
case AscendNorth, AscendSouth, AscendEast, AscendWest:
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) {
return false
}
return SteppableBlock(nav.World.GetBlockStatus(x, y, z)) &&
AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+1, from.Z)) &&
AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+2, from.Z))
case DescendLadder, AscendLadder:
if bID := nav.World.GetBlockStatus(x, y+1, z); !AirLikeBlock(bID) && !IsLadder(bID) {
return false
}
return IsLadder(nav.World.GetBlockStatus(x, y, z))
case DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest:
for amt := 0; amt < 2; amt++ {
if bID := nav.World.GetBlockStatus(x, y+amt+1, z); !AirLikeBlock(bID) && !IsLadder(bID) {
return false
}
}
return IsLadder(nav.World.GetBlockStatus(x, y, z))
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+3, z)) {
return false
}
midX, midZ := (from.X+x)/2, (from.Z+z)/2
if !AirLikeBlock(nav.World.GetBlockStatus(midX, y+1, midZ)) || !AirLikeBlock(nav.World.GetBlockStatus(midX, y+2, midZ)) || !AirLikeBlock(nav.World.GetBlockStatus(midX, y+3, midZ)) {
return false
}
if !AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+3, from.Z)) {
return false
}
return SteppableBlock(nav.World.GetBlockStatus(x, y, z))
default:
panic(m)
}
}
func (m Movement) Offset() (x, y, z int) {
switch m {
case Waypoint:
return 0, 0, 0
case TraverseNorth:
return North.Offset()
case TraverseSouth:
return South.Offset()
case TraverseEast:
return East.Offset()
case TraverseWest:
return West.Offset()
case JumpCrossEast:
return East.Offset2x()
case JumpCrossWest:
return West.Offset2x()
case JumpCrossNorth:
return North.Offset2x()
case JumpCrossSouth:
return South.Offset2x()
case AscendLadder:
return 0, 1, 0
case DescendLadder:
return 0, -1, 0
case DropNorth, DescendLadderNorth:
return 0, -1, -1
case DropSouth, DescendLadderSouth:
return 0, -1, 1
case DropEast, DescendLadderEast:
return 1, -1, 0
case DropWest, DescendLadderWest:
return -1, -1, 0
case AscendNorth:
return 0, 1, -1
case AscendSouth:
return 0, 1, 1
case AscendEast:
return 1, 1, 0
case AscendWest:
return -1, 1, 0
case TraverseNorthWest:
return -1, 0, -1
case TraverseNorthEast:
return 1, 0, -1
case TraverseSouthWest:
return -1, 0, 1
case TraverseSouthEast:
return 1, 0, 1
case Drop2North:
return 0, -2, -1
case Drop2South:
return 0, -2, 1
case Drop2East:
return 1, -2, 0
case Drop2West:
return -1, -2, 0
default:
panic(m)
}
}
func (m Movement) BaseCost() float64 {
switch m {
case Waypoint:
return 0
case TraverseNorth, TraverseSouth, TraverseEast, TraverseWest:
return 2
case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast:
return 2.5
case DropNorth, DropSouth, DropEast, DropWest, Drop2North, Drop2South, Drop2East, Drop2West:
return 4
case AscendNorth, AscendSouth, AscendEast, AscendWest:
return 4
case DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest:
return 1.5
case DescendLadder:
return 1
case AscendLadder:
return 3
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
return 5.5
default:
panic(m)
}
}
func (m Movement) String() string {
switch m {
case Waypoint:
return "waypoint"
case TraverseNorth:
return "traverse-north"
case TraverseSouth:
return "traverse-south"
case TraverseEast:
return "traverse-east"
case TraverseWest:
return "traverse-west"
case DropNorth:
return "drop-north"
case DropSouth:
return "drop-south"
case DropEast:
return "drop-east"
case DropWest:
return "drop-west"
case Drop2North:
return "drop-2-north"
case Drop2South:
return "drop-2-south"
case Drop2East:
return "drop-2-east"
case Drop2West:
return "drop-2-west"
case AscendNorth:
return "jump-north"
case AscendSouth:
return "jump-south"
case AscendEast:
return "jump-east"
case AscendWest:
return "jump-west"
case TraverseNorthWest:
return "traverse-northwest"
case TraverseNorthEast:
return "traverse-northeast"
case TraverseSouthWest:
return "traverse-southwest"
case TraverseSouthEast:
return "traverse-southeast"
case DescendLadder:
return "descend-ladder"
case DescendLadderNorth:
return "descend-ladder-north"
case DescendLadderSouth:
return "descend-ladder-south"
case DescendLadderEast:
return "descend-ladder-east"
case DescendLadderWest:
return "descend-ladder-west"
case AscendLadder:
return "ascend-ladder"
case JumpCrossEast:
return "jump-crossing-east"
case JumpCrossWest:
return "jump-crossing-west"
case JumpCrossNorth:
return "jump-crossing-north"
case JumpCrossSouth:
return "jump-crossing-south"
default:
panic(m)
}
}

View File

@ -1,208 +0,0 @@
// Package path implements pathfinding.
package path
import (
"math"
"math/rand"
"time"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
"github.com/beefsack/go-astar"
)
// Point represents a point in 3D space.
type Point struct {
X, Y, Z float64
}
type V3 struct {
X, Y, Z int
}
func (v V3) Cost(other V3) float64 {
x, y, z := v.X-other.X, v.Y-other.Y, v.Z-other.Z
return math.Sqrt(float64(x*x+z*z)) + math.Sqrt(1.2*float64(y*y))
}
// Nav represents a navigation to a position.
type Nav struct {
World *world.World
Start, Dest V3
}
func (n *Nav) Path() (path []astar.Pather, distance float64, found bool) {
return astar.Path(
Tile{ // Start point
Nav: n,
Movement: Waypoint,
Pos: n.Start,
},
Tile{ // Destination point
Nav: n,
Movement: Waypoint,
Pos: n.Dest,
})
}
// Tile represents a point in a path. All tiles in a path are adjaceent their
// preceeding tiles.
type Tile struct {
Nav *Nav
HalfBlock bool
Movement Movement
Pos V3
BlockStatus world.BlockStatus
ExtraCost int
}
func (t Tile) PathNeighborCost(to astar.Pather) float64 {
other := to.(Tile)
return 1 + other.Movement.BaseCost()
}
func (t Tile) PathEstimatedCost(to astar.Pather) float64 {
other := to.(Tile)
cost := t.Pos.Cost(other.Pos)
return cost + other.Movement.BaseCost()
}
func (t Tile) PathNeighbors() []astar.Pather {
possibles := make([]astar.Pather, 0, 8)
if c := t.PathEstimatedCost(Tile{Pos: t.Nav.Start}); c > 8000 {
return nil
}
if t.Pos == t.Nav.Dest && t.Movement != Waypoint {
dupe := t
dupe.Movement = Waypoint
dupe.BlockStatus = 0
return []astar.Pather{dupe}
}
for _, m := range allMovements {
x, y, z := m.Offset()
pos := V3{X: t.Pos.X + x, Y: t.Pos.Y + y, Z: t.Pos.Z + z}
possible := m.Possible(t.Nav, pos.X, pos.Y, pos.Z, t.Pos, t.Movement)
if possible {
bStateID := t.Nav.World.GetBlockStatus(pos.X, pos.Y, pos.Z)
possibles = append(possibles, Tile{
Nav: t.Nav,
Movement: m,
Pos: pos,
BlockStatus: bStateID,
HalfBlock: IsSlab(bStateID) && SlabIsBottom(bStateID),
})
}
}
return possibles
}
func (t Tile) Inputs(pos, deltaPos, vel Point, runTime time.Duration) Inputs {
// Sufficient for simple movements.
at := math.Atan2(-deltaPos.X, -deltaPos.Z)
mdX, _, mdZ := t.Movement.Offset()
wantYaw := -math.Atan2(float64(mdX), float64(mdZ)) * 180 / math.Pi
out := Inputs{
ThrottleX: math.Sin(at),
ThrottleZ: math.Cos(at),
Yaw: wantYaw,
}
if mdX == 0 && mdZ == 0 {
out.Yaw = math.NaN()
}
if (rand.Int() % 14) == 0 {
out.Pitch = float64((rand.Int() % 4) - 2)
}
switch t.Movement {
case DescendLadder, DescendLadderEast, DescendLadderWest, DescendLadderNorth, DescendLadderSouth:
// Deadzone the throttle to prevent an accidental ascend.
if dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z); dist2 < (0.22 * 0.22 * 2) {
out.ThrottleX, out.ThrottleZ = 0, 0
}
case AscendLadder:
dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z)
if x, _, z := LadderDirection(t.BlockStatus).Offset(); dist2 > (0.8*0.8) && deltaPos.Y < 0 {
pos.X -= (0.25 * float64(x))
pos.Z -= (0.25 * float64(z))
} else {
pos.X += (0.42 * float64(x))
pos.Z += (0.42 * float64(z))
}
at = math.Atan2(-pos.X+float64(t.Pos.X)+0.5, -pos.Z+float64(t.Pos.Z)+0.5)
out = Inputs{
ThrottleX: math.Sin(at),
ThrottleZ: math.Cos(at),
Yaw: math.NaN(),
}
case AscendNorth, AscendSouth, AscendEast, AscendWest:
var (
b = block.ByID[block.StateID[uint32(t.BlockStatus)]]
_, isStairs = stairs[b.ID]
_, isSlab = slabs[b.ID]
maybeStuck = runTime < 1250*time.Millisecond
dist2 = math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z)
)
out.Jump = dist2 < 1.75 && deltaPos.Y < -0.81
switch {
case isStairs:
// Special logic for stairs: Try to go towards the downwards edge initially.
if dist2 > (0.9*0.9) && deltaPos.Y < 0 {
if x, _, z := StairsDirection(t.BlockStatus).Offset(); dist2 > (0.9*0.9) && deltaPos.Y < 0 {
pos.X += (0.49 * float64(x))
pos.Z += (0.49 * float64(z))
}
at = math.Atan2(-pos.X+float64(t.Pos.X)+0.5, -pos.Z+float64(t.Pos.Z)+0.5)
out = Inputs{
ThrottleX: math.Sin(at),
ThrottleZ: math.Cos(at),
Yaw: math.NaN(),
Jump: out.Jump && !maybeStuck,
}
}
// We dont need to jump for slabs, so only jump if we get stuck.
case isSlab:
out.Jump = out.Jump && !maybeStuck
}
// Turn off the throttle if we get stuck on the jump.
if dist2 < 1 && deltaPos.Y < 0 && vel.Y == 0 {
out.ThrottleX, out.ThrottleZ = 0, 0
}
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z)
out.Jump = dist2 > 1.5 && dist2 < 1.78
}
return out
}
func (t Tile) IsComplete(d Point) bool {
switch t.Movement {
case DescendLadder, DescendLadderNorth, DescendLadderSouth, DescendLadderWest, DescendLadderEast,
DropNorth, DropSouth, DropEast, DropWest:
return (d.X*d.X+d.Z*d.Z) < (2*0.2*0.25) && d.Y <= 0.05
case AscendLadder:
return d.Y >= 0
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
return (d.X*d.X+d.Z*d.Z) < (0.22*0.22) && d.Y >= -0.065
}
yLowerCutoff := -0.065
if t.HalfBlock {
yLowerCutoff -= 0.5
}
return (d.X*d.X+d.Z*d.Z) < (0.18*0.18) && d.Y >= yLowerCutoff && d.Y <= 0.08
}

View File

@ -1,143 +0,0 @@
package phy
import (
"math"
"github.com/Tnze/go-mc/bot/world"
)
type MinMax struct {
Min, Max float64
}
// Extends adjusts the bounds of the MinMax. A negative number will reduce the
// minimum bound, whereas a positive number will increase the maximum bound.
func (mm MinMax) Extend(delta float64) MinMax {
if delta < 0 {
return MinMax{
Min: mm.Min + delta,
Max: mm.Max,
}
}
return MinMax{
Min: mm.Min,
Max: mm.Max + delta,
}
}
// Contract reduces both the minimum and maximum bound by the provided amount,
// such that the difference between the bounds decreases for positive values.
func (mm MinMax) Contract(amt float64) MinMax {
return MinMax{
Min: mm.Min + amt,
Max: mm.Max - amt,
}
}
// Expand changes the minimum and maximum bounds by the provided amount, such
// that the difference between the bounds increases for positive values.
func (mm MinMax) Expand(amt float64) MinMax {
return MinMax{
Min: mm.Min - amt,
Max: mm.Max + amt,
}
}
// Offset adds the provided value to both the minimum and maximum value.
func (mm MinMax) Offset(amt float64) MinMax {
return MinMax{
Min: mm.Min + amt,
Max: mm.Max + amt,
}
}
// AABB implements Axis Aligned Bounding Box operations.
type AABB struct {
X, Y, Z MinMax
Block world.BlockStatus
}
// Extend adjusts the minimum (for negative values) or maximum bounds (for
// positive values) by the provided scalar for each dimension.
func (bb AABB) Extend(dx, dy, dz float64) AABB {
return AABB{
X: bb.X.Extend(dx),
Y: bb.Y.Extend(dx),
Z: bb.Z.Extend(dx),
Block: bb.Block,
}
}
// Contract reduces the difference between the min/max bounds (for positive
// values) for each dimension.
func (bb AABB) Contract(x, y, z float64) AABB {
return AABB{
X: bb.X.Contract(x),
Y: bb.Y.Contract(y),
Z: bb.Z.Contract(z),
Block: bb.Block,
}
}
// Expand increases both the minimum and maximum bounds by the provided amount
// (for positive values) for each dimension.
func (bb AABB) Expand(x, y, z float64) AABB {
return AABB{
X: bb.X.Expand(x),
Y: bb.Y.Expand(y),
Z: bb.Z.Expand(z),
Block: bb.Block,
}
}
// Offset moves both the minimum and maximum bound by the provided value for
// each dimension.
func (bb AABB) Offset(x, y, z float64) AABB {
return AABB{
X: bb.X.Offset(x),
Y: bb.Y.Offset(y),
Z: bb.Z.Offset(z),
Block: bb.Block,
}
}
func (bb AABB) XOffset(o AABB, xOffset float64) float64 {
if o.Y.Max > bb.Y.Min && o.Y.Min < bb.Y.Max && o.Z.Max > bb.Z.Min && o.Z.Min < bb.Z.Max {
if xOffset > 0.0 && o.X.Max <= bb.X.Min {
xOffset = math.Min(bb.X.Min-o.X.Max, xOffset)
} else if xOffset < 0.0 && o.X.Min >= bb.X.Max {
xOffset = math.Max(bb.X.Max-o.X.Min, xOffset)
}
}
return xOffset
}
func (bb AABB) YOffset(o AABB, yOffset float64) float64 {
if o.X.Max > bb.X.Min && o.X.Min < bb.X.Max && o.Z.Max > bb.Z.Min && o.Z.Min < bb.Z.Max {
if yOffset > 0.0 && o.Y.Max <= bb.Y.Min {
yOffset = math.Min(bb.Y.Min-o.Y.Max, yOffset)
} else if yOffset < 0.0 && o.Y.Min >= bb.Y.Max {
yOffset = math.Max(bb.Y.Max-o.Y.Min, yOffset)
}
}
return yOffset
}
func (bb AABB) ZOffset(o AABB, zOffset float64) float64 {
if o.X.Max > bb.X.Min && o.X.Min < bb.X.Max && o.Y.Max > bb.Y.Min && o.Y.Min < bb.Y.Max {
if zOffset > 0.0 && o.Z.Max <= bb.Z.Min {
zOffset = math.Min(bb.Z.Min-o.Z.Max, zOffset)
} else if zOffset < 0.0 && o.Z.Min >= bb.Z.Max {
zOffset = math.Max(bb.Z.Max-o.Z.Min, zOffset)
}
}
return zOffset
}
func (bb AABB) Intersects(o AABB) bool {
return true &&
bb.X.Min < o.X.Max && bb.X.Max > o.X.Min &&
bb.Y.Min < o.Y.Max && bb.Y.Max > o.Y.Min &&
bb.Z.Min < o.Z.Max && bb.Z.Max > o.Z.Min
}

View File

@ -1,288 +0,0 @@
// Package phy implements a minimal physics simulation necessary for realistic
// bot behavior.
package phy
import (
"math"
"github.com/Tnze/go-mc/bot/path"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/bot/world/entity/player"
"github.com/Tnze/go-mc/data/block/shape"
)
const (
playerWidth = 0.6
playerHeight = 1.8
resetVel = 0.003
maxYawChange = 11
maxPitchChange = 7
stepHeight = 0.6
minJumpTicks = 14
ladderMaxSpeed = 0.15
ladderClimbSpeed = 0.2
gravity = 0.08
drag = 0.98
acceleration = 0.02
inertia = 0.91
slipperiness = 0.6
)
// World represents a provider of information about the surrounding world.
type World interface {
GetBlockStatus(x, y, z int) world.BlockStatus
}
// Surrounds represents the blocks surrounding the player (Y, Z, X).
type Surrounds []AABB
// State tracks physics state.
type State struct {
// player state.
Pos path.Point
Vel path.Point
Yaw, Pitch float64
lastJump uint32
// player state flags.
onGround bool
collision struct {
vertical bool
horizontal bool
}
tick uint32
Run bool
}
func (s *State) ServerPositionUpdate(player player.Pos, w World) error {
s.Pos = path.Point{X: player.X, Y: player.Y, Z: player.Z}
s.Yaw, s.Pitch = float64(player.Yaw), float64(player.Pitch)
s.Vel = path.Point{}
s.onGround, s.collision.vertical, s.collision.horizontal = false, false, false
s.Run = true
return nil
}
func abs(i1, i2 int) int {
if i1 < i2 {
return i2 - i1
}
return i1 - i2
}
func (s *State) surroundings(query AABB, w World) Surrounds {
minY, maxY := int(math.Floor(query.Y.Min))-1, int(math.Floor(query.Y.Max))+1
minZ, maxZ := int(math.Floor(query.Z.Min)), int(math.Floor(query.Z.Max))+1
minX, maxX := int(math.Floor(query.X.Min)), int(math.Floor(query.X.Max))+1
out := Surrounds(make([]AABB, 0, abs(maxY, minY)*abs(maxZ, minZ)*abs(maxX, minX)*2))
for y := minY; y < maxY; y++ {
for z := minZ; z < maxZ; z++ {
for x := minX; x < maxX; x++ {
bStateID := w.GetBlockStatus(x, y, z)
if !path.AirLikeBlock(bStateID) {
bbs, err := shape.CollisionBoxes(bStateID)
if err != nil {
panic(err)
}
for _, box := range bbs {
out = append(out, AABB{
X: MinMax{Min: box.Min.X, Max: box.Max.X},
Y: MinMax{Min: box.Min.Y, Max: box.Max.Y},
Z: MinMax{Min: box.Min.Z, Max: box.Max.Z},
Block: bStateID,
}.Offset(float64(x), float64(y), float64(z)))
}
}
}
}
}
return out
}
func (s *State) BB() AABB {
return AABB{
X: MinMax{Min: -playerWidth / 2, Max: playerWidth / 2},
Y: MinMax{Max: playerHeight},
Z: MinMax{Min: -playerWidth / 2, Max: playerWidth / 2},
}.Offset(s.Pos.X, s.Pos.Y, s.Pos.Z)
}
func (s *State) Position() player.Pos {
return player.Pos{
X: s.Pos.X, Y: s.Pos.Y, Z: s.Pos.Z,
Yaw: float32(s.Yaw), Pitch: float32(s.Pitch),
OnGround: s.onGround,
}
}
func (s *State) Tick(input path.Inputs, w World) error {
s.tick++
if !s.Run {
return nil
}
var inertia = inertia
var acceleration = acceleration
if below := w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y))-1, int(math.Floor(s.Pos.Z))); s.onGround && !path.AirLikeBlock(below) {
inertia *= slipperiness
acceleration = 0.1 * (0.1627714 / (inertia * inertia * inertia))
}
s.tickVelocity(input, inertia, acceleration, w)
s.tickPosition(w)
if path.IsLadder(w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y)), int(math.Floor(s.Pos.Z)))) && s.collision.horizontal {
s.Vel.Y = ladderClimbSpeed
}
// Gravity
s.Vel.Y -= gravity
// Drag & friction.
s.Vel.Y *= drag
s.Vel.X *= inertia
s.Vel.Z *= inertia
return nil
}
func (s *State) tickVelocity(input path.Inputs, inertia, acceleration float64, w World) {
// Deadzone velocities when they get too low.
if math.Abs(s.Vel.X) < resetVel {
s.Vel.X = 0
}
if math.Abs(s.Vel.Y) < resetVel {
s.Vel.Y = 0
}
if math.Abs(s.Vel.Z) < resetVel {
s.Vel.Z = 0
}
s.applyLookInputs(input)
s.applyPosInputs(input, acceleration, inertia)
lower := w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y)), int(math.Floor(s.Pos.Z)))
if path.IsLadder(lower) {
s.Vel.X = math.Min(math.Max(-ladderMaxSpeed, s.Vel.X), ladderMaxSpeed)
s.Vel.Z = math.Min(math.Max(-ladderMaxSpeed, s.Vel.Z), ladderMaxSpeed)
s.Vel.Y = math.Min(math.Max(-ladderMaxSpeed, s.Vel.Y), ladderMaxSpeed)
}
}
func (s *State) applyLookInputs(input path.Inputs) {
if !math.IsNaN(input.Yaw) {
errYaw := math.Min(math.Max(modYaw(input.Yaw, s.Yaw), -maxYawChange), maxYawChange)
s.Yaw += errYaw
}
errPitch := math.Min(math.Max(input.Pitch-s.Pitch, -maxPitchChange), maxPitchChange)
s.Pitch += errPitch
}
func (s *State) applyPosInputs(input path.Inputs, acceleration, inertia float64) {
if input.Jump && s.lastJump+minJumpTicks < s.tick && s.onGround {
s.lastJump = s.tick
s.Vel.Y = 0.42
}
speed := math.Sqrt(input.ThrottleX*input.ThrottleX + input.ThrottleZ*input.ThrottleZ)
if speed < 0.01 {
return
}
speed = acceleration / math.Max(speed, 1)
input.ThrottleX *= speed
input.ThrottleZ *= speed
s.Vel.X += input.ThrottleX
s.Vel.Z += input.ThrottleZ
}
func (s *State) tickPosition(w World) {
p, newVel := s.computeCollisionYXZ(s.BB(), s.BB().Offset(s.Vel.X, s.Vel.Y, s.Vel.Z), s.Vel, w)
if s.onGround || (s.Vel.Y != newVel.Y && s.Vel.Y < 0) {
bb := s.BB()
surroundings := s.surroundings(bb.Offset(s.Vel.X, stepHeight, s.Vel.Z), w)
outVel := s.Vel
outVel.Y = stepHeight
for _, b := range surroundings {
outVel.Y = b.YOffset(bb, outVel.Y)
}
bb = bb.Offset(0, outVel.Y, 0)
for _, b := range surroundings {
outVel.X = b.XOffset(bb, outVel.X)
}
bb = bb.Offset(outVel.X, 0, 0)
for _, b := range surroundings {
outVel.Z = b.ZOffset(bb, outVel.Z)
}
bb = bb.Offset(0, 0, outVel.Z)
outVel.Y *= -1
// Lower the player back down to be on the ground.
for _, b := range surroundings {
outVel.Y = b.YOffset(bb, outVel.Y)
}
bb = bb.Offset(0, outVel.Y, 0)
oldMove := newVel.X*newVel.X + newVel.Z*newVel.Z
newMove := outVel.X*outVel.X + outVel.Z*outVel.Z
if oldMove >= newMove || outVel.Y <= (0.000002-stepHeight) {
// fmt.Println("nope")
} else {
p = bb
newVel = outVel
}
}
// Update flags.
s.Pos.X = p.X.Min + playerWidth/2
s.Pos.Y = p.Y.Min
s.Pos.Z = p.Z.Min + playerWidth/2
s.collision.horizontal = newVel.X != s.Vel.X || newVel.Z != s.Vel.Z
s.collision.vertical = newVel.Y != s.Vel.Y
s.onGround = s.collision.vertical && s.Vel.Y < 0
s.Vel = newVel
}
func modYaw(new, old float64) float64 {
delta := math.Mod(new-old, 360)
if delta > 180 {
delta = 180 - delta
} else if delta < -180 {
delta += 360
}
return delta
}
func (s *State) computeCollisionYXZ(bb, query AABB, vel path.Point, w World) (outBB AABB, outVel path.Point) {
surroundings := s.surroundings(query, w)
outVel = vel
for _, b := range surroundings {
outVel.Y = b.YOffset(bb, outVel.Y)
}
bb = bb.Offset(0, outVel.Y, 0)
for _, b := range surroundings {
outVel.X = b.XOffset(bb, outVel.X)
}
bb = bb.Offset(outVel.X, 0, 0)
for _, b := range surroundings {
outVel.Z = b.ZOffset(bb, outVel.Z)
}
bb = bb.Offset(0, 0, outVel.Z)
return bb, outVel
}
// AtLookTarget returns true if the player look position is actually at the
// given pitch and yaw.
func (s *State) AtLookTarget(yaw, pitch float64) bool {
dYaw, dPitch := math.Abs(modYaw(yaw, s.Yaw)), math.Abs(pitch-s.Pitch)
return dYaw <= 0.8 && dPitch <= 1.1
}

View File

@ -2,9 +2,12 @@ package bot
import (
"fmt"
"net"
"strconv"
"time"
"github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/data/packetid"
mcnet "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet"
)
@ -12,49 +15,70 @@ import (
// Returns a JSON data with server status, and the delay.
//
// For more information for JSON format, see https://wiki.vg/Server_List_Ping#Response
func PingAndList(addr string, port int) ([]byte, time.Duration, error) {
conn, err := net.DialMC(fmt.Sprintf("%s:%d", addr, port))
func PingAndList(addr string) ([]byte, time.Duration, error) {
addrSrv, err := parseAddress(&net.Resolver{}, addr)
if err != nil {
return nil, 0, fmt.Errorf("bot: dial fail: %v", err)
return nil, 0, LoginErr{"parse address", err}
}
return pingAndList(addr, port, conn)
conn, err := mcnet.DialMC(addrSrv)
if err != nil {
return nil, 0, LoginErr{"dial connection", err}
}
return pingAndList(addr, conn)
}
// PingAndListTimeout PingAndLIstTimeout is the version of PingAndList with max request time.
func PingAndListTimeout(addr string, port int, timeout time.Duration) ([]byte, time.Duration, error) {
func PingAndListTimeout(addr string, timeout time.Duration) ([]byte, time.Duration, error) {
deadLine := time.Now().Add(timeout)
conn, err := net.DialMCTimeout(fmt.Sprintf("%s:%d", addr, port), timeout)
addrSrv, err := parseAddress(&net.Resolver{}, addr)
if err != nil {
return nil, 0, fmt.Errorf("bot: dial fail: %v", err)
return nil, 0, LoginErr{"parse address", err}
}
conn, err := mcnet.DialMCTimeout(addrSrv, timeout)
if err != nil {
return nil, 0, err
}
err = conn.Socket.SetDeadline(deadLine)
if err != nil {
return nil, 0, fmt.Errorf("bot: set deadline fail: %v", err)
return nil, 0, LoginErr{"set deadline", err}
}
return pingAndList(addr, port, conn)
return pingAndList(addr, conn)
}
func pingAndList(addr string, port int, conn *net.Conn) ([]byte, time.Duration, error) {
func pingAndList(addr string, conn *mcnet.Conn) ([]byte, time.Duration, error) {
// Split Host and Port
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return nil, 0, LoginErr{"split address", err}
}
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, 0, LoginErr{"parse port", err}
}
const Handshake = 0x00
//握手
err := conn.WritePacket(
//Handshake Packet
pk.Marshal(
0x00, //Handshake packet ID
pk.VarInt(ProtocolVersion), //Protocol version
pk.String(addr), //Server's address
pk.UnsignedShort(port),
pk.Byte(1),
))
err = conn.WritePacket(pk.Marshal(
Handshake, //Handshake packet ID
pk.VarInt(ProtocolVersion), //Protocol version
pk.String(host), //Server's address
pk.UnsignedShort(port),
pk.Byte(1),
))
if err != nil {
return nil, 0, fmt.Errorf("bot: send handshake packect fail: %v", err)
}
//LIST
//请求服务器状态
err = conn.WritePacket(pk.Marshal(0))
err = conn.WritePacket(pk.Marshal(
packetid.PingStart,
))
if err != nil {
return nil, 0, fmt.Errorf("bot: send list packect fail: %v", err)
}
@ -72,7 +96,10 @@ func pingAndList(addr string, port int, conn *net.Conn) ([]byte, time.Duration,
//PING
startTime := time.Now()
err = conn.WritePacket(pk.Marshal(0x01, pk.Long(startTime.Unix())))
err = conn.WritePacket(pk.Marshal(
packetid.PingServerbound,
pk.Long(startTime.Unix()),
))
if err != nil {
return nil, 0, fmt.Errorf("bot: send ping packect fail: %v", err)
}

View File

@ -1,38 +0,0 @@
package world
// bitArray implements a bitfield array where values are packed into uint64
// values. If the next value does not fit into remaining space, the remaining
// space of a uint64 is unused.
type bitArray struct {
width uint // bit width of each value
valuesPerElement uint // number of values which fit into a single uint64.
data []uint64
}
// Size returns the number of elements that can fit into the bit array.
func (b *bitArray) Size() int {
return int(b.valuesPerElement) * len(b.data)
}
func (b *bitArray) Set(idx, val uint) {
var (
arrayIdx = idx / b.valuesPerElement
startBit = (idx % b.valuesPerElement) * b.width
mask = ^uint64((1<<b.width - 1) << startBit) // set for all bits except target
)
b.data[arrayIdx] = (b.data[arrayIdx] & mask) | uint64(val<<startBit)
}
func (b *bitArray) Get(idx uint) uint {
var (
arrayIdx = idx / b.valuesPerElement
offset = (idx % b.valuesPerElement) * b.width
mask = uint64((1<<b.width - 1) << offset) // set for just the target
)
return uint(b.data[arrayIdx]&mask) >> offset
}
func valuesPerBitArrayElement(bitsPerValue uint) uint {
return uint(64 / bitsPerValue)
}

View File

@ -1,50 +0,0 @@
package world
import (
"encoding/binary"
"encoding/hex"
"testing"
)
func TestBitArrayBasic(t *testing.T) {
a := bitArray{
width: 5,
valuesPerElement: valuesPerBitArrayElement(5),
data: make([]uint64, 5),
}
if got, want := a.Size(), 12*5; got != want {
t.Errorf("size = %d, want %d", got, want)
}
a.Set(0, 4)
if v := a.Get(0); v != 4 {
t.Errorf("v[0] = %d, want 4", v)
}
a.Set(12, 8)
if v := a.Get(12); v != 8 {
t.Errorf("v[12] = %d, want 8", v)
}
}
func TestBitArrayHardcoded(t *testing.T) {
d1, _ := hex.DecodeString("0020863148418841")
d2, _ := hex.DecodeString("01018A7260F68C87")
a := bitArray{
width: 5,
valuesPerElement: valuesPerBitArrayElement(5),
data: []uint64{binary.BigEndian.Uint64(d1), binary.BigEndian.Uint64(d2)},
}
if got, want := a.Size(), 12*2; got != want {
t.Errorf("size = %d, want %d", got, want)
}
want := []uint{1, 2, 2, 3, 4, 4, 5, 6, 6, 4, 8, 0, 7, 4, 3, 13, 15, 16, 9, 14, 10, 12, 0, 2}
for idx, want := range want {
if got := a.Get(uint(idx)); got != want {
t.Errorf("v[%d] = %d, want %d", idx, got, want)
}
}
}

View File

@ -1,173 +0,0 @@
package world
import (
"bytes"
"fmt"
"io"
"math"
"github.com/Tnze/go-mc/data/block"
pk "github.com/Tnze/go-mc/net/packet"
)
const maxPaletteBits = 8
// DecodeChunkColumn decode the chunk data structure.
// If decoding went error, successful decoded data will be returned.
func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) {
var c Chunk
r := bytes.NewReader(data)
for sectionY := 0; sectionY < 16; sectionY++ {
// If the section's bit set in the mask
if (mask & (1 << uint(sectionY))) != 0 {
// read section
sec, err := readSection(r)
if err != nil {
return &c, fmt.Errorf("read section[%d] error: %w", sectionY, err)
}
c.Sections[sectionY] = sec
}
}
return &c, nil
}
func perBits(bpb byte) uint {
switch {
case bpb <= 4:
return 4
case bpb <= maxPaletteBits:
return uint(bpb)
default:
return uint(block.BitsPerBlock)
}
}
func readSection(data io.Reader) (s Section, err error) {
var nonAirBlockCount pk.Short
if _, err := nonAirBlockCount.ReadFrom(data); err != nil {
return nil, fmt.Errorf("block count: %w", err)
}
var bpb pk.UnsignedByte
if _, err := bpb.ReadFrom(data); err != nil {
return nil, fmt.Errorf("bits per block: %w", err)
}
// If bpb values greater than or equal to 9, use directSection.
// Otherwise use paletteSection.
var palettes []BlockStatus
var palettesIndex map[BlockStatus]int
if bpb <= maxPaletteBits {
// read palettes
var length pk.VarInt
if _, err := length.ReadFrom(data); err != nil {
return nil, fmt.Errorf("palette length: %w", err)
}
palettes = make([]BlockStatus, length)
palettesIndex = make(map[BlockStatus]int, length)
for i := 0; i < int(length); i++ {
var v pk.VarInt
if _, err := v.ReadFrom(data); err != nil {
return nil, fmt.Errorf("read palettes[%d] error: %w", i, err)
}
palettes[i] = BlockStatus(v)
palettesIndex[BlockStatus(v)] = i
}
}
// read data array
var dataLen pk.VarInt
if _, err := dataLen.ReadFrom(data); err != nil {
return nil, fmt.Errorf("read data array length error: %w", err)
}
if int(dataLen) < 16*16*16*int(bpb)/64 {
return nil, fmt.Errorf("data length (%d) is not enough of given bpb (%d)", dataLen, bpb)
}
dataArray := make([]uint64, dataLen)
for i := 0; i < int(dataLen); i++ {
var v pk.Long
if _, err := v.ReadFrom(data); err != nil {
return nil, fmt.Errorf("read dataArray[%d] error: %w", i, err)
}
dataArray[i] = uint64(v)
}
width := perBits(byte(bpb))
sec := directSection{
bitArray{
width: width,
valuesPerElement: valuesPerBitArrayElement(width),
data: dataArray,
},
}
if bpb <= maxPaletteBits {
return &paletteSection{
palette: palettes,
palettesIndex: palettesIndex,
directSection: sec,
}, nil
} else {
return &sec, nil
}
}
type directSection struct {
bitArray
}
func (d *directSection) GetBlock(offset uint) BlockStatus {
return BlockStatus(d.Get(offset))
}
func (d *directSection) SetBlock(offset uint, s BlockStatus) {
d.Set(offset, uint(s))
}
func (d *directSection) CanContain(s BlockStatus) bool {
return s <= (1<<d.width - 1)
}
func (d *directSection) clone(bpb uint) *directSection {
out := newSectionWithSize(bpb)
for offset := uint(0); offset < 16*16*16; offset++ {
out.SetBlock(offset, d.GetBlock(offset))
}
return out
}
func newSectionWithSize(bpb uint) *directSection {
valuesPerElement := valuesPerBitArrayElement(bpb)
return &directSection{
bitArray{
width: bpb,
valuesPerElement: valuesPerElement,
data: make([]uint64, int(math.Ceil(16*16*16/float64(valuesPerElement)))),
},
}
}
type paletteSection struct {
palette []BlockStatus
palettesIndex map[BlockStatus]int
directSection
}
func (p *paletteSection) GetBlock(offset uint) BlockStatus {
v := p.directSection.GetBlock(offset)
return p.palette[v]
}
func (p *paletteSection) SetBlock(offset uint, s BlockStatus) {
if i, ok := p.palettesIndex[s]; ok {
p.directSection.SetBlock(offset, BlockStatus(i))
return
}
i := len(p.palette)
p.palette = append(p.palette, s)
p.palettesIndex[s] = i
if !p.directSection.CanContain(BlockStatus(i)) {
// Increase the underlying directSection
// Suppose that old bpb fit len(p.palette) before it appended.
// So bpb+1 must enough for new len(p.palette).
p.directSection = *p.directSection.clone(p.width + 1)
}
p.directSection.SetBlock(offset, BlockStatus(i))
}

View File

@ -1,95 +0,0 @@
package entity
import (
"io"
"github.com/Tnze/go-mc/data/entity"
item "github.com/Tnze/go-mc/data/item"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
)
// BlockEntity describes the representation of a tile entity at a position.
type BlockEntity struct {
ID string `nbt:"id"`
// global co-ordinates
X int `nbt:"x"`
Y int `nbt:"y"`
Z int `nbt:"z"`
// sign-specific.
Color string `nbt:"color"`
Text1 string `nbt:"Text1"`
Text2 string `nbt:"Text2"`
Text3 string `nbt:"Text3"`
Text4 string `nbt:"Text4"`
}
//Entity represents an instance of an entity.
type Entity struct {
ID int32
Data int32
Base *entity.Entity
UUID uuid.UUID
X, Y, Z float64
Pitch, Yaw int8
VelX, VelY, VelZ int16
OnGround bool
IsLiving bool
HeadPitch int8
}
// The Slot data structure is how Minecraft represents an item and its associated data in the Minecraft Protocol
type Slot struct {
Present bool
ItemID item.ID
Count int8
NBT pk.NBT
}
type SlotNBT struct {
data interface{}
}
//Decode implement packet.FieldDecoder interface
func (s *Slot) ReadFrom(r io.Reader) (int64, error) {
var itemID pk.VarInt
n, err := pk.Tuple{
(*pk.Boolean)(&s.Present),
pk.Opt{
Has: (*pk.Boolean)(&s.Present),
Field: pk.Tuple{
&itemID,
(*pk.Byte)(&s.Count),
&s.NBT,
},
},
}.ReadFrom(r)
if err != nil {
return n, err
}
s.ItemID = item.ID(itemID)
return n, nil
}
func (s Slot) WriteTo(w io.Writer) (int64, error) {
return pk.Tuple{
pk.Boolean(s.Present),
pk.Opt{
Has: (*pk.Boolean)(&s.Present),
Field: pk.Tuple{
pk.VarInt(s.ItemID),
pk.Byte(s.Count),
s.NBT,
},
},
}.WriteTo(w)
}
func (s Slot) String() string {
return item.ByID[s.ItemID].DisplayName
}

View File

@ -1,35 +0,0 @@
package player
import "github.com/Tnze/go-mc/bot/world/entity"
type Pos struct {
X, Y, Z float64
Yaw, Pitch float32
OnGround bool
}
func (p Pos) PosEqual(other Pos) bool {
return p.X == other.X && p.Y == other.Y && p.Z == other.Z
}
func (p Pos) LookEqual(other Pos) bool {
return p.Yaw == other.Yaw && p.Pitch == other.Pitch
}
func (p Pos) Equal(other Pos) bool {
return p.PosEqual(other) && p.LookEqual(other) && p.OnGround == other.OnGround
}
// Player includes the player's status.
type Player struct {
entity.Entity
UUID [2]int64 //128bit UUID
Pos Pos
HeldItem int //拿着的物品栏位
Health float32 //血量
Food int32 //饱食度
FoodSaturation float32 //食物饱和度
Level int32
}

View File

@ -1,21 +0,0 @@
package world
import (
"fmt"
)
// TilePosition describes the location of a tile/block entity within a chunk.
type TilePosition uint32
func (p TilePosition) Pos() (x, y, z int) {
return int((p >> 8) & 0xff), int((p >> 16) & 0xff), int(p & 0xff)
}
func (p TilePosition) String() string {
x, y, z := p.Pos()
return fmt.Sprintf("(%d, %d, %d)", x, y, z)
}
func ToTilePos(x, y, z int) TilePosition {
return TilePosition((y&0xff)<<16 | (x&15)<<8 | (z & 15))
}

View File

@ -1,15 +0,0 @@
package world
import (
"sync"
"github.com/Tnze/go-mc/bot/world/entity"
)
// World record all of the things in the world where player at
type World struct {
entityLock sync.RWMutex
Entities map[int32]*entity.Entity
chunkLock sync.RWMutex
Chunks map[ChunkLoc]*Chunk
}

View File

@ -1,152 +0,0 @@
package world
import (
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/data/block"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
)
// Chunk store a 256*16*16 area of blocks, sharded on the Y axis into 16
// sections.
type Chunk struct {
Sections [16]Section
TileEntities map[TilePosition]entity.BlockEntity
}
// Section implements storage of blocks within a fixed 16*16*16 area.
type Section interface {
// GetBlock return block status, offset can be calculate by SectionOffset.
GetBlock(offset uint) BlockStatus
// SetBlock is the reverse operation of GetBlock.
SetBlock(offset uint, s BlockStatus)
}
func sectionIdx(x, y, z int) uint {
return uint(((y & 15) << 8) | (z << 4) | x)
}
type BlockStatus uint32
type ChunkLoc struct {
X, Z int
}
// Signs returns a list of signs on the server within viewing range.
func (w *World) Signs() []entity.BlockEntity {
w.chunkLock.RLock()
defer w.chunkLock.RUnlock()
out := make([]entity.BlockEntity, 0, 4)
for _, c := range w.Chunks {
for _, e := range c.TileEntities {
if e.ID == "minecraft:sign" {
out = append(out, e)
}
}
}
return out
}
// GetBlockStatus return the state ID of the block at the given position.
func (w *World) GetBlockStatus(x, y, z int) BlockStatus {
w.chunkLock.RLock()
defer w.chunkLock.RUnlock()
c := w.Chunks[ChunkLoc{x >> 4, z >> 4}]
if c != nil && y >= 0 {
if sec := c.Sections[y>>4]; sec != nil {
return sec.GetBlock(sectionIdx(x&15, y&15, z&15))
}
}
return 0
}
// UnloadChunk unloads a chunk from the world.
func (w *World) UnloadChunk(loc ChunkLoc) {
w.chunkLock.Lock()
delete(w.Chunks, loc)
w.chunkLock.Unlock()
}
// UnaryBlockUpdate updates the block at the specified position with a new
// state ID.
func (w *World) UnaryBlockUpdate(pos pk.Position, bStateID BlockStatus) bool {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
c := w.Chunks[ChunkLoc{X: pos.X >> 4, Z: pos.Z >> 4}]
if c == nil {
return false
}
sIdx, bIdx := pos.Y>>4, sectionIdx(pos.X&15, pos.Y&15, pos.Z&15)
if sec := c.Sections[sIdx]; sec == nil {
sec = newSectionWithSize(uint(block.BitsPerBlock))
sec.SetBlock(bIdx, bStateID)
c.Sections[sIdx] = sec
} else {
tp := ToTilePos(pos.X, pos.Y, pos.Z)
if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID {
delete(c.TileEntities, tp)
}
sec.SetBlock(bIdx, bStateID)
}
return true
}
// MultiBlockUpdate updates a packed specification of blocks within a single
// section of a chunk.
func (w *World) MultiBlockUpdate(loc ChunkLoc, sectionY int, blocks []pk.VarLong) bool {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
c := w.Chunks[loc]
if c == nil {
return false // not loaded
}
sec := c.Sections[sectionY]
if sec == nil {
sec = newSectionWithSize(uint(block.BitsPerBlock))
c.Sections[sectionY] = sec
}
for _, b := range blocks {
var (
bStateID = BlockStatus(b >> 12)
x, z, y = (b >> 8) & 0xf, (b >> 4) & 0xf, b & 0xf
bIdx = sectionIdx(int(x&15), int(y&15), int(z&15))
tp = ToTilePos(int(x), int(y), int(z))
)
if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID {
delete(c.TileEntities, tp)
}
sec.SetBlock(bIdx, bStateID)
}
return true
}
//LoadChunk adds the given chunk to the world.
func (w *World) LoadChunk(x, z int, c *Chunk) {
w.chunkLock.Lock()
w.Chunks[ChunkLoc{X: x, Z: z}] = c
w.chunkLock.Unlock()
}
func (w *World) TileEntityUpdate(pkt ptypes.TileEntityData) error {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
c := w.Chunks[ChunkLoc{X: pkt.Pos.X >> 4, Z: pkt.Pos.Z >> 4}]
if c == nil {
return nil
}
switch pkt.Action {
case 9: // Sign update
c.TileEntities[ToTilePos(pkt.Pos.X, pkt.Pos.Y, pkt.Pos.Z)] = pkt.Data
}
return nil
}

View File

@ -1,165 +0,0 @@
package world
import (
"fmt"
"github.com/Tnze/go-mc/bot/world/entity"
e "github.com/Tnze/go-mc/data/entity"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
"github.com/google/uuid"
)
// PlayerEntities returns a list of players on the server within viewing range.
func (w *World) PlayerEntities() []entity.Entity {
w.entityLock.RLock()
defer w.entityLock.RUnlock()
out := make([]entity.Entity, 0, 12)
for _, ent := range w.Entities {
if ent.Base.ID == e.Player.ID {
out = append(out, *ent)
}
}
return out
}
// OnSpawnEntity should be called when a SpawnEntity packet
// is received.
func (w *World) OnSpawnEntity(pkt ptypes.SpawnEntity) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
base, ok := e.ByID[e.ID(pkt.Type)]
if !ok {
return fmt.Errorf("unknown entity ID %v", pkt.Type)
}
w.Entities[int32(pkt.ID)] = &entity.Entity{
ID: int32(pkt.ID),
Base: base,
Data: int32(pkt.Data),
UUID: uuid.UUID(pkt.UUID),
X: float64(pkt.X),
Y: float64(pkt.Y),
Z: float64(pkt.Z),
Pitch: int8(pkt.Pitch),
Yaw: int8(pkt.Yaw),
VelX: int16(pkt.VelX),
VelY: int16(pkt.VelY),
VelZ: int16(pkt.VelZ),
}
return nil
}
// OnSpawnLivingEntity should be called when a SpawnLivingEntity packet
// is received.
func (w *World) OnSpawnLivingEntity(pkt ptypes.SpawnLivingEntity) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
base, ok := e.ByID[e.ID(pkt.Type)]
if !ok {
return fmt.Errorf("unknown entity ID %v", pkt.Type)
}
w.Entities[int32(pkt.ID)] = &entity.Entity{
ID: int32(pkt.ID),
Base: base,
UUID: uuid.UUID(pkt.UUID),
X: float64(pkt.X),
Y: float64(pkt.Y),
Z: float64(pkt.Z),
Pitch: int8(pkt.Pitch),
Yaw: int8(pkt.Yaw),
VelX: int16(pkt.VelX),
VelY: int16(pkt.VelY),
VelZ: int16(pkt.VelZ),
HeadPitch: int8(pkt.HeadPitch),
}
return nil
}
// OnSpawnPlayer should be called when a SpawnPlayer packet
// is received.
func (w *World) OnSpawnPlayer(pkt ptypes.SpawnPlayer) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
w.Entities[int32(pkt.ID)] = &entity.Entity{
ID: int32(pkt.ID),
Base: &e.Player,
UUID: uuid.UUID(pkt.UUID),
X: float64(pkt.X),
Y: float64(pkt.Y),
Z: float64(pkt.Z),
Pitch: int8(pkt.Pitch),
Yaw: int8(pkt.Yaw),
}
return nil
}
// OnEntityPosUpdate should be called when an EntityPosition packet
// is received.
func (w *World) OnEntityPosUpdate(pkt ptypes.EntityPosition) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
ent, ok := w.Entities[int32(pkt.ID)]
if !ok {
return fmt.Errorf("cannot handle position update for unknown entity %d", pkt.ID)
}
ent.X += float64(pkt.X) / (128 * 32)
ent.Y += float64(pkt.Y) / (128 * 32)
ent.Z += float64(pkt.Z) / (128 * 32)
ent.OnGround = bool(pkt.OnGround)
return nil
}
// OnEntityPosLookUpdate should be called when an EntityPositionLook packet
// is received.
func (w *World) OnEntityPosLookUpdate(pkt ptypes.EntityPositionLook) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
ent, ok := w.Entities[int32(pkt.ID)]
if !ok {
return fmt.Errorf("cannot handle position look update for unknown entity %d", pkt.ID)
}
ent.X += float64(pkt.X) / (128 * 32)
ent.Y += float64(pkt.Y) / (128 * 32)
ent.Z += float64(pkt.Z) / (128 * 32)
ent.OnGround = bool(pkt.OnGround)
ent.Pitch, ent.Yaw = int8(pkt.Pitch), int8(pkt.Yaw)
return nil
}
// OnEntityLookUpdate should be called when an EntityRotation packet
// is received.
func (w *World) OnEntityLookUpdate(pkt ptypes.EntityRotation) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
ent, ok := w.Entities[int32(pkt.ID)]
if !ok {
return fmt.Errorf("cannot handle look update for unknown entity %d", pkt.ID)
}
ent.Pitch, ent.Yaw = int8(pkt.Pitch), int8(pkt.Yaw)
ent.OnGround = bool(pkt.OnGround)
return nil
}
// OnEntityDestroy should be called when a DestroyEntities packet
// is received.
func (w *World) OnEntityDestroy(eIDs []pk.VarInt) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
for _, eID := range eIDs {
delete(w.Entities, int32(eID))
}
return nil
}

View File

@ -1,49 +1,61 @@
package main
import (
"bytes"
"errors"
"flag"
"log"
"github.com/google/uuid"
"github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/bot/basic"
"github.com/Tnze/go-mc/chat"
_ "github.com/Tnze/go-mc/data/lang/zh-cn"
pk "github.com/Tnze/go-mc/net/packet"
)
var address = flag.String("address", "127.0.0.1", "The server address")
var c *bot.Client
var client *bot.Client
var player *basic.Player
func main() {
flag.Parse()
c = bot.NewClient()
client = bot.NewClient()
player = basic.NewPlayer(client, basic.DefaultSettings)
basic.EventsListener{
GameStart: onGameStart,
ChatMsg: onChatMsg,
Disconnect: onDisconnect,
Death: onDeath,
}.Attach(client)
//Login
err := c.JoinServer(*address)
err := client.JoinServer(*address)
if err != nil {
log.Fatal(err)
}
log.Println("Login success")
//Register event handlers
c.Events.GameStart = onGameStart
c.Events.ChatMsg = onChatMsg
c.Events.Disconnect = onDisconnect
c.Events.PluginMessage = onPluginMessage
//JoinGame
err = c.HandleGame()
if err != nil {
log.Fatal(err)
for {
if err = client.HandleGame(); err != nil {
var interErr *bot.PacketHandlerError
if errors.As(err, &interErr) {
log.Print("Internal bugs: ", interErr)
} else {
log.Fatal(err)
}
} else {
break
}
}
}
func onDeath() error {
log.Println("Died and Respawned")
// If we exclude Respawn(...) then the player won't press the "Respawn" button upon death
return c.Respawn()
return player.Respawn()
}
func onGameStart() error {
@ -56,22 +68,7 @@ func onChatMsg(c chat.Message, pos byte, uuid uuid.UUID) error {
return nil
}
func onDisconnect(c chat.Message) error {
log.Println("Disconnect:", c)
return nil
}
func onPluginMessage(channel string, data []byte) error {
switch channel {
case "minecraft:brand":
var brand pk.String
if _, err := brand.ReadFrom(bytes.NewReader(data)); err != nil {
return err
}
log.Println("Server brand is:", brand)
default:
log.Println("PluginMessage", channel, data)
}
func onDisconnect(reason chat.Message) error {
log.Println("Disconnect:", reason)
return nil
}

View File

@ -5,12 +5,10 @@ import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/chat"
_ "github.com/Tnze/go-mc/data/lang/en-us"
"github.com/google/uuid"
)
@ -32,11 +30,9 @@ type status struct {
}
func main() {
addr, port := getAddr()
fmt.Printf("MCPING (%s:%d):\n", addr, port)
resp, delay, err := bot.PingAndList(addr, port)
addr := getAddr()
fmt.Printf("MCPING (%s):\n", addr)
resp, delay, err := bot.PingAndList(addr)
if err != nil {
fmt.Printf("ping and list server fail: %v", err)
os.Exit(1)
@ -53,28 +49,14 @@ func main() {
fmt.Println("Delay:", delay)
}
func getAddr() (string, int) {
func getAddr() string {
const usage = "Usage: mcping <hostname>[:port]"
if len(os.Args) < 2 {
fmt.Println("no host name.", usage)
os.Exit(1)
}
addr := strings.Split(os.Args[1], ":")
var port int
switch len(addr) {
case 1:
port = 25565
case 2:
var err error
port, err = strconv.Atoi(addr[1])
if err != nil {
fmt.Println(err, usage)
os.Exit(1)
}
}
return addr[0], port
return os.Args[1]
}
func (s status) String() string {

View File

@ -19,27 +19,35 @@ const (
package packetid
// Valid PktID values.
// Login state
const (
// Clientbound packets for connections in the login state.
// Clientbound
{{range $Name, $ID := .Login.Clientbound}} {{$Name}} = {{$ID}}
{{end}}
// Serverbound packets for connections in the login state
// Serverbound
{{range $Name, $ID := .Login.Serverbound}} {{$Name}} = {{$ID}}
{{end}}
// Clientbound packets for connections in the play state.
{{range $Name, $ID := .Play.Clientbound}} {{$Name}} = {{$ID}}
{{end}}
// Serverbound packets for connections in the play state.
{{range $Name, $ID := .Play.Serverbound}} {{$Name}} = {{$ID}}
{{end}}
// Clientbound packets used to respond to ping/status requests.
)
// Ping state
const (
// Clientbound
{{range $Name, $ID := .Status.Clientbound}} {{$Name}} = {{$ID}}
{{end}}
// Serverbound packets used to ping or read server status.
// Serverbound
{{range $Name, $ID := .Status.Serverbound}} {{$Name}} = {{$ID}}
{{end}}
)
// Play state
const (
// Clientbound
{{range $Name, $ID := .Play.Clientbound}} {{$Name}} = {{$ID}}
{{end}}
// Serverbound
{{range $Name, $ID := .Play.Serverbound}} {{$Name}} = {{$ID}}
{{end}}
)
`
)

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.16
require (
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482
github.com/fatih/set v0.2.1 // indirect
github.com/google/uuid v1.1.1
github.com/iancoleman/strcase v0.1.3
github.com/mattn/go-colorable v0.1.8

2
go.sum
View File

@ -1,5 +1,7 @@
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 h1:p4g4uok3+r6Tg6fxXEQUAcMAX/WdK6WhkQW9s0jaT7k=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw=

View File

@ -30,7 +30,7 @@ func IsArrayTag(ty byte) bool {
}
type DecoderReader = interface {
io.ByteScanner
io.ByteReader
io.Reader
}
type Decoder struct {
@ -42,7 +42,7 @@ func NewDecoder(r io.Reader) *Decoder {
if br, ok := r.(DecoderReader); ok {
d.r = br
} else {
d.r = bufio.NewReader(r)
d.r = bufio.NewReaderSize(r, 0)
}
return d
}

View File

@ -36,10 +36,7 @@ func (l Listener) Accept() (Conn, error) {
//Conn is a minecraft Connection
type Conn struct {
Socket net.Conn
Reader interface {
io.ByteReader
io.Reader
}
io.Reader
io.Writer
threshold int
@ -50,7 +47,7 @@ func DialMC(addr string) (*Conn, error) {
conn, err := net.Dial("tcp", addr)
return &Conn{
Socket: conn,
Reader: bufio.NewReader(conn),
Reader: conn,
Writer: conn,
}, err
}
@ -60,7 +57,7 @@ func DialMCTimeout(addr string, timeout time.Duration) (*Conn, error) {
conn, err := net.DialTimeout("tcp", addr, timeout)
return &Conn{
Socket: conn,
Reader: bufio.NewReader(conn),
Reader: conn,
Writer: conn,
}, err
}
@ -70,7 +67,7 @@ func DialMCTimeout(addr string, timeout time.Duration) (*Conn, error) {
func WrapConn(conn net.Conn) *Conn {
return &Conn{
Socket: conn,
Reader: bufio.NewReader(conn),
Reader: conn,
Writer: conn,
}
}
@ -91,10 +88,10 @@ func (c *Conn) WritePacket(p pk.Packet) error {
// SetCipher load the decode/encode stream to this Conn
func (c *Conn) SetCipher(ecoStream, decoStream cipher.Stream) {
//加密连接
c.Reader = bufio.NewReader(cipher.StreamReader{ //Set receiver for AES
c.Reader = cipher.StreamReader{ //Set receiver for AES
S: decoStream,
R: c.Socket,
})
}
c.Writer = cipher.StreamWriter{
S: ecoStream,
W: c.Socket,

Binary file not shown.

View File

@ -3,8 +3,10 @@ package packet
import (
"bytes"
"compress/zlib"
"encoding/hex"
"fmt"
"io"
"os"
)
// Packet define a net data package
@ -25,8 +27,9 @@ func Marshal(id int32, fields ...FieldEncoder) (pk Packet) {
//Scan decode the packet and fill data into fields
func (p Packet) Scan(fields ...FieldDecoder) error {
r := bytes.NewReader(p.Data)
rr := io.TeeReader(r, hex.Dumper(os.Stdout))
for _, v := range fields {
_, err := v.ReadFrom(r)
_, err := v.ReadFrom(rr)
if err != nil {
return err
}
@ -82,25 +85,25 @@ func (p *Packet) UnPack(r io.Reader, threshold int) error {
if length < 1 {
return fmt.Errorf("packet length too short")
}
buf := bytes.NewBuffer(p.Data[:0])
if _, err := io.CopyN(buf, r, int64(length)); err != nil {
buf := make([]byte, length)
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("read content of packet fail: %w", err)
}
buffer := bytes.NewBuffer(buf)
//解压数据
if threshold > 0 {
if err := unCompress(buf); err != nil {
if err := unCompress(buffer); err != nil {
return err
}
}
var packetID VarInt
if _, err := packetID.ReadFrom(buf); err != nil {
if _, err := packetID.ReadFrom(buffer); err != nil {
return fmt.Errorf("read packet id fail: %v", err)
}
p.ID = int32(packetID)
p.Data = buf.Bytes()
p.Data = buffer.Bytes()
return nil
}

View File

@ -1,11 +1,14 @@
package packet
package packet_test
import (
"bytes"
_ "embed"
"testing"
pk "github.com/Tnze/go-mc/net/packet"
)
var VarInts = []VarInt{0, 1, 2, 127, 128, 255, 2147483647, -1, -2147483648}
var VarInts = []pk.VarInt{0, 1, 2, 127, 128, 255, 2147483647, -1, -2147483648}
var PackedVarInts = [][]byte{
{0x00},
@ -35,7 +38,7 @@ func TestPackVarInt(t *testing.T) {
}
func TestUnpackVarInt(t *testing.T) {
for i, v := range PackedVarInts {
var vi VarInt
var vi pk.VarInt
if _, err := vi.ReadFrom(bytes.NewReader(v)); err != nil {
t.Errorf("unpack \"% x\" error: %v", v, err)
}
@ -46,7 +49,7 @@ func TestUnpackVarInt(t *testing.T) {
}
func TestUnpackVarInt_TooLongData(t *testing.T) {
var vi VarInt
var vi pk.VarInt
var data = []byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01}
if _, err := vi.ReadFrom(bytes.NewReader(data)); err != nil {
t.Logf("unpack \"% x\" error: %v", data, err)
@ -55,7 +58,7 @@ func TestUnpackVarInt_TooLongData(t *testing.T) {
}
}
var VarLongs = []VarLong{0, 1, 2, 127, 128, 255, 2147483647, 9223372036854775807, -1, -2147483648, -9223372036854775808}
var VarLongs = []pk.VarLong{0, 1, 2, 127, 128, 255, 2147483647, 9223372036854775807, -1, -2147483648, -9223372036854775808}
var PackedVarLongs = [][]byte{
{0x00},
@ -81,7 +84,7 @@ func TestPackVarLong(t *testing.T) {
}
func TestUnpackVarLong(t *testing.T) {
for i, v := range PackedVarLongs {
var vi VarLong
var vi pk.VarLong
if _, err := vi.ReadFrom(bytes.NewReader(v)); err != nil {
t.Errorf("unpack \"% x\" error: %v", v, err)
}
@ -90,3 +93,46 @@ func TestUnpackVarLong(t *testing.T) {
}
}
}
//go:embed joingame_test.bin
var joingame []byte
func TestJoinGamePacket(t *testing.T) {
p := pk.Packet{ID: 0x24, Data: joingame}
var (
EID pk.Int
Hardcore pk.Boolean
Gamemode pk.UnsignedByte
PreGamemode pk.Byte
WorldCount pk.VarInt
WorldNames = pk.Ary{Len: &WorldCount, Ary: &[]pk.String{}}
DimensionCodec struct {
DimensionType interface{} `nbt:"minecraft:dimension_type"`
WorldgenBiome interface{} `nbt:"minecraft:worldgen/biome"`
}
Dimension interface{}
WorldName pk.Identifier
HashedSeed pk.Long
MaxPlayers pk.VarInt
ViewDistance pk.VarInt
RDI, ERS, IsDebug, IsFlat pk.Boolean
)
err := p.Scan(
&EID,
&Hardcore,
&Gamemode,
&PreGamemode,
&WorldCount,
WorldNames,
&pk.NBT{V: &DimensionCodec},
&pk.NBT{V: &Dimension},
&WorldName,
&HashedSeed,
&MaxPlayers,
&ViewDistance,
&RDI, &ERS, &IsDebug, &IsFlat,
)
if err != nil {
t.Error(err)
}
}

View File

@ -6,13 +6,13 @@ import (
)
type Ary struct {
Len Field // One of VarInt, VarLong, Int or Long
Ary interface{} // FieldEncoder, FieldDecoder or both (Field)
Len Field // Pointer of VarInt, VarLong, Int or Long
Ary interface{} // Slice of FieldEncoder, FieldDecoder or both (Field)
}
func (a Ary) WriteTo(r io.Writer) (n int64, err error) {
length := int(reflect.ValueOf(a.Len).Int())
array := reflect.ValueOf(a.Ary).Elem()
array := reflect.ValueOf(a.Ary)
for i := 0; i < length; i++ {
elem := array.Index(i)
nn, err := elem.Interface().(FieldEncoder).WriteTo(r)
@ -25,11 +25,15 @@ func (a Ary) WriteTo(r io.Writer) (n int64, err error) {
}
func (a Ary) ReadFrom(r io.Reader) (n int64, err error) {
length := int(reflect.ValueOf(a.Len).Int())
length := int(reflect.ValueOf(a.Len).Elem().Int())
array := reflect.ValueOf(a.Ary).Elem()
if array.Cap() < length {
array = reflect.MakeSlice(array.Type(), length, length)
a.Ary = array.Interface()
}
for i := 0; i < length; i++ {
elem := array.Index(i)
nn, err := elem.Interface().(FieldDecoder).ReadFrom(r)
nn, err := elem.Addr().Interface().(FieldDecoder).ReadFrom(r)
n += nn
if err != nil {
return n, err

View File

@ -1,82 +0,0 @@
// Package ptypes implements encoding and decoding for high-level packets.
package ptypes
import (
"io"
"math"
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/nbt"
pk "github.com/Tnze/go-mc/net/packet"
)
// ChunkData is a client-bound packet which describes a chunk.
type ChunkData struct {
X, Z pk.Int
FullChunk pk.Boolean
PrimaryBitMask pk.VarInt
Heightmaps struct{}
Biomes biomesData
Data pk.ByteArray
BlockEntities blockEntities
}
func (p *ChunkData) ReadFrom(r io.Reader) (int64, error) {
return pk.Tuple{
&p.X,
&p.Z,
&p.FullChunk,
&p.PrimaryBitMask,
&pk.NBT{V: &p.Heightmaps},
pk.Opt{Has: &p.FullChunk, Field: &p.Biomes},
&p.Data,
&p.BlockEntities,
}.ReadFrom(r)
}
type biomesData struct {
data []pk.VarInt
}
func (b *biomesData) ReadFrom(r io.Reader) (int64, error) {
var n pk.VarInt // Number of Biomes Data
return pk.Tuple{
&n, pk.Ary{Len: &n, Ary: []pk.VarInt{}},
}.ReadFrom(r)
}
type blockEntities []entity.BlockEntity
// Decode implement net.packet.FieldDecoder
func (b *blockEntities) ReadFrom(r io.Reader) (n int64, err error) {
var sz pk.VarInt // Number of BlockEntities
if nn, err := sz.ReadFrom(r); err != nil {
return nn, err
} else {
n += nn
}
*b = make(blockEntities, sz)
lr := &io.LimitedReader{R: r, N: math.MaxInt64}
d := nbt.NewDecoder(lr)
for i := 0; i < int(sz); i++ {
if err := d.Decode(&(*b)[i]); err != nil {
return math.MaxInt64 - lr.N, err
}
}
return math.MaxInt64 - lr.N, nil
}
// TileEntityData describes a change to a tile entity.
type TileEntityData struct {
Pos pk.Position
Action pk.UnsignedByte
Data entity.BlockEntity
}
func (p *TileEntityData) ReadFrom(r io.Reader) (int64, error) {
return pk.Tuple{
&p.Pos,
&p.Action,
&pk.NBT{V: &p.Data},
}.ReadFrom(r)
}

View File

@ -1,95 +0,0 @@
package ptypes
import pk "github.com/Tnze/go-mc/net/packet"
// SpawnEntity is a client-bound packet used to spawn a non-mob entity.
type SpawnEntity struct {
ID pk.VarInt
UUID pk.UUID
Type pk.VarInt
X, Y, Z pk.Double
Pitch, Yaw pk.Angle
Data pk.Int
VelX, VelY, VelZ pk.Short
}
func (p *SpawnEntity) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.UUID, &p.Type,
&p.X, &p.Y, &p.Z, &p.Pitch, &p.Yaw,
&p.Data, &p.VelX, &p.VelY, &p.VelZ)
}
// SpawnPlayer is a client-bound packet used to describe a player entering
// visible range.
type SpawnPlayer struct {
ID pk.VarInt
UUID pk.UUID
X, Y, Z pk.Double
Yaw, Pitch pk.Angle
}
func (p *SpawnPlayer) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.UUID, &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch)
}
// SpawnLivingEntity is a client-bound packet used to spawn a mob.
type SpawnLivingEntity struct {
ID pk.VarInt
UUID pk.UUID
Type pk.VarInt
X, Y, Z pk.Double
Yaw, Pitch pk.Angle
HeadPitch pk.Angle
VelX, VelY, VelZ pk.Short
}
func (p *SpawnLivingEntity) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.UUID, &p.Type,
&p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch,
&p.HeadPitch, &p.VelX, &p.VelY, &p.VelZ)
}
// EntityAnimationClientbound updates the animation state of an entity.
type EntityAnimationClientbound struct {
ID pk.VarInt
Animation pk.UnsignedByte
}
func (p *EntityAnimationClientbound) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.Animation)
}
// EntityPosition is a clientbound packet used to update an entities position.
type EntityPosition struct {
ID pk.VarInt
X, Y, Z pk.Short // Deltas
OnGround pk.Boolean
}
func (p *EntityPosition) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.X, &p.Y, &p.Z, &p.OnGround)
}
// EntityPosition is a clientbound packet used to update an entities position
// and its rotation.
type EntityPositionLook struct {
ID pk.VarInt
X, Y, Z pk.Short // Deltas
Yaw, Pitch pk.Angle
OnGround pk.Boolean
}
func (p *EntityPositionLook) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, &p.OnGround)
}
// EntityRotation is a clientbound packet used to update an entities rotation.
type EntityRotation struct {
ID pk.VarInt
Yaw, Pitch pk.Angle
OnGround pk.Boolean
}
func (p *EntityRotation) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.Yaw, &p.Pitch, &p.OnGround)
}

View File

@ -1,81 +0,0 @@
// Package ptypes implements encoding and decoding for high-level packets.
package ptypes
import (
"errors"
"io"
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/nbt"
pk "github.com/Tnze/go-mc/net/packet"
)
// SetSlot is a clientbound packet which configures an inventory slot.
// A window ID of -1 represents the cursor, and a window ID of 0 represents
// the players inventory.
type SetSlot struct {
WindowID pk.Byte
Slot pk.Short
SlotData entity.Slot
}
func (p *SetSlot) Decode(pkt pk.Packet) error {
if err := pkt.Scan(&p.WindowID, &p.Slot, &p.SlotData); err != nil && !errors.Is(err, nbt.ErrEND) {
return err
}
return nil
}
// WindowItems is a clientbound packet describing the contents of multiple
// inventory slots in a window/inventory.
type WindowItems struct {
WindowID pk.Byte
Slots []entity.Slot
}
func (p *WindowItems) ReadFrom(r io.Reader) (int64, error) {
var count pk.Short
return pk.Tuple{
&p.WindowID,
&count,
&pk.Ary{
Len: &count,
Ary: []entity.Slot{},
},
}.ReadFrom(r)
}
// OpenWindow is a clientbound packet which opens an inventory.
type OpenWindow struct {
WindowID pk.VarInt
WindowType pk.VarInt
Title chat.Message
}
func (p *OpenWindow) Decode(pkt pk.Packet) error {
if err := pkt.Scan(&p.WindowID, &p.WindowType, &p.Title); err != nil && !errors.Is(err, nbt.ErrEND) {
return err
}
return nil
}
type ConfirmTransaction struct {
WindowID pk.Byte
ActionID pk.Short
Accepted pk.Boolean
}
func (p *ConfirmTransaction) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.WindowID, &p.ActionID, &p.Accepted)
}
func (p ConfirmTransaction) Encode() pk.Packet {
return pk.Marshal(
packetid.TransactionServerbound,
p.WindowID,
p.ActionID,
p.Accepted,
)
}

View File

@ -1,92 +0,0 @@
package ptypes
import (
"io"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
)
// SoundEffect is a client-bound packet used to play a specific sound ID
// on the client.
type SoundEffect struct {
Sound pk.VarInt
Category pk.VarInt
X, Y, Z pk.Int
Volume, Pitch pk.Float
}
func (p *SoundEffect) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Sound, &p.Category, &p.X, &p.Y, &p.Z, &p.Volume, &p.Pitch)
}
// NamedSoundEffect is a client-bound packet used to play a sound with the
// specified name on the client.
type NamedSoundEffect struct {
Sound pk.String
Category pk.VarInt
X, Y, Z pk.Int
Volume, Pitch pk.Float
}
func (p *NamedSoundEffect) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Sound, &p.Category, &p.X, &p.Y, &p.Z, &p.Volume, &p.Pitch)
}
// ChatMessageClientbound represents a chat message forwarded by the server.
type ChatMessageClientbound struct {
S chat.Message
Pos pk.Byte
Sender pk.UUID
}
func (p *ChatMessageClientbound) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.S, &p.Pos, &p.Sender)
}
// UpdateHealth encodes player health/food information from the server.
type UpdateHealth struct {
Health pk.Float
Food pk.VarInt
FoodSaturation pk.Float
}
func (p *UpdateHealth) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Health, &p.Food, &p.FoodSaturation)
}
// PluginData encodes the custom data encoded in a plugin message.
type PluginData []byte
func (p PluginData) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(p)
return int64(n), err
}
func (p *PluginData) ReadFrom(r io.Reader) (int64, error) {
d, err := io.ReadAll(r)
if err != nil {
return int64(len(d)), err
}
*p = d
return int64(len(d)), nil
}
// PluginMessage represents a packet with a customized payload.
type PluginMessage struct {
Channel pk.Identifier
Data PluginData
}
func (p *PluginMessage) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Channel, &p.Data)
}
func (p *PluginMessage) Encode() pk.Packet {
return pk.Marshal(
packetid.CustomPayloadServerbound,
p.Channel,
p.Data,
)
}

View File

@ -1,81 +0,0 @@
// Package ptypes implements encoding and decoding for high-level packets.
package ptypes
import (
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
)
// PositionAndLookClientbound describes the location and orientation of
// the player.
type PositionAndLookClientbound struct {
X, Y, Z pk.Double
Yaw, Pitch pk.Float
Flags pk.Byte
TeleportID pk.VarInt
}
func (p *PositionAndLookClientbound) RelativeX() bool {
return p.Flags&0x01 != 0
}
func (p *PositionAndLookClientbound) RelativeY() bool {
return p.Flags&0x02 != 0
}
func (p *PositionAndLookClientbound) RelativeZ() bool {
return p.Flags&0x04 != 0
}
func (p *PositionAndLookClientbound) RelativeYaw() bool {
return p.Flags&0x08 != 0
}
func (p *PositionAndLookClientbound) RelativePitch() bool {
return p.Flags&0x10 != 0
}
func (p *PositionAndLookClientbound) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, &p.Flags, &p.TeleportID)
}
// PositionAndLookServerbound describes the location and orientation of
// the player.
type PositionAndLookServerbound struct {
X, Y, Z pk.Double
Yaw, Pitch pk.Float
OnGround pk.Boolean
}
func (p PositionAndLookServerbound) Encode() pk.Packet {
return pk.Marshal(
packetid.PositionLook,
p.X, p.Y, p.Z,
p.Yaw, p.Pitch,
p.OnGround,
)
}
// Position describes the position of the player.
type Position struct {
X, Y, Z pk.Double
OnGround pk.Boolean
}
func (p Position) Encode() pk.Packet {
return pk.Marshal(
packetid.PositionServerbound,
p.X, p.Y, p.Z,
p.OnGround,
)
}
// Look describes the rotation of the player.
type Look struct {
Yaw, Pitch pk.Float
OnGround pk.Boolean
}
func (p Look) Encode() pk.Packet {
return pk.Marshal(
packetid.Look,
p.Yaw, p.Pitch,
p.OnGround,
)
}

View File

@ -1,32 +0,0 @@
package ptypes
import (
pk "github.com/Tnze/go-mc/net/packet"
)
// JoinGame encodes global/world information from the server.
type JoinGame struct {
PlayerEntity pk.Int
Hardcore pk.Boolean
Gamemode pk.UnsignedByte
PrevGamemode pk.UnsignedByte
WorldCount pk.VarInt
WorldNames pk.Identifier
//DimensionCodec pk.NBT
Dimension pk.Int
WorldName pk.Identifier
HashedSeed pk.Long
maxPlayers pk.VarInt // Now ignored
ViewDistance pk.VarInt
RDI pk.Boolean // Reduced Debug Info
ERS pk.Boolean // Enable respawn screen
IsDebug pk.Boolean
IsFlat pk.Boolean
}
func (p *JoinGame) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.PlayerEntity, &p.Hardcore, &p.Gamemode, &p.PrevGamemode,
&p.WorldCount, &p.WorldNames, &p.Dimension,
&p.WorldName, &p.HashedSeed, &p.maxPlayers, &p.ViewDistance,
&p.RDI, &p.ERS, &p.IsDebug, &p.IsFlat)
}