add player package with key client-side player functionality, update protocol codecs, and refactor metadata definitions and slot usage

This commit is contained in:
2025-08-23 16:19:45 +08:00
parent cff9d4a809
commit 4528bdc86a
32 changed files with 2613 additions and 468 deletions

4
go.mod
View File

@ -7,6 +7,10 @@ toolchain go1.24.4
require (
github.com/Tnze/go-mc v1.20.3-0.20241224032005-539b4a3a7f03
github.com/google/uuid v1.6.0
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476
golang.org/x/sync v0.16.0
)
require github.com/go-gl/mathgl v1.2.0
replace github.com/Tnze/go-mc v1.20.3-0.20241224032005-539b4a3a7f03 => git.konjactw.dev/patyhank/go-mc v1.20.3-0.20250619063151-133e3fab4ac2

18
go.sum
View File

@ -1,16 +1,10 @@
git.konjactw.dev/patyhank/go-mc v1.20.3-0.20250619063151-133e3fab4ac2 h1:KiA1OsQQGjrKxev45URJPwvyuVwen9Bb4TzjEg/ojz8=
git.konjactw.dev/patyhank/go-mc v1.20.3-0.20250619063151-133e3fab4ac2/go.mod h1:e3pBU8tqRfYDHrhtZRtyfGdYijA86b1fF3XgnEDSgHk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY=
github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mlange-42/ark v0.4.3 h1:k5MydLdgONyOaHT0mfTJNW68kkHz24hkMnmzog6SOjw=
github.com/mlange-42/ark v0.4.3/go.mod h1:47KXHr5HLftLn4iyL8w04iv7KJUNUoDymEIotD41f3o=
github.com/mlange-42/ark-tools v0.1.4 h1:GD+CYe+jx7b/HQCgdHNym04IwxsAfTdPgKskoM3Mwpw=
github.com/mlange-42/ark-tools v0.1.4/go.mod h1:+uS2tMZrMXRtPfRRN0S6F/ou7XbivqcFV/Lc214XRCQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=

381
pkg/auth/auth.go Normal file
View File

@ -0,0 +1,381 @@
package auth
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/login/client"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/login/server"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/net/CFB8"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
)
var (
ErrLogin = errors.New("login")
ErrKick = errors.New("login.kicked")
ErrEncrypt = errors.New("login.encrypt")
)
type Profile struct {
Name string
UUID uuid.UUID
}
type Provider interface {
Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error
FetchProfile(ctx context.Context) *Profile
}
type Auth struct {
*net.Conn
Provider
}
func (a *Auth) HandleLogin(ctx context.Context) error {
profile := a.FetchProfile(ctx)
err := a.WritePacket(pk.Marshal(packetid.ServerboundLoginHello, server.LoginHello{
Name: profile.Name,
UUID: profile.UUID,
}))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("write login hello fail: %w", err))
}
var p pk.Packet
for {
err = a.ReadPacket(&p)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read packet fail: %w", err))
}
switch packetid.ClientboundPacketID(p.ID) {
case packetid.ClientboundLoginLoginDisconnect:
var reason chat.JsonMessage
err = p.Scan(&reason)
return errors.Join(ErrKick, fmt.Errorf("kicked by server: %s", chat.Message(reason).ClearString()))
case packetid.ClientboundLoginHello:
var hello client.LoginHello
err = p.Scan(&hello)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login hello fail: %w", err))
}
err = a.Authenticate(ctx, a.Conn, hello)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("authenticate fail: %w", err))
}
case packetid.ClientboundLoginLoginFinished:
err = a.WritePacket(pk.Marshal(packetid.ServerboundLoginLoginAcknowledged))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("write login ack fail: %w", err))
}
return nil
case packetid.ClientboundLoginLoginCompression:
var threshold int32
err = p.Scan((*pk.VarInt)(&threshold))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login compression fail: %w", err))
}
a.Conn.SetThreshold(int(threshold))
case packetid.ClientboundLoginCustomQuery:
var query client.LoginCustomQuery
err = p.Scan(&query)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login custom query fail: %w", err))
}
err = a.WritePacket(pk.Marshal(
packetid.ServerboundLoginCustomQueryAnswer,
&server.LoginCustomQueryAnswer{MessageID: query.MessageID},
))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login custom query fail: %w", err))
}
case packetid.ClientboundLoginCookieRequest:
var cookie client.LoginCookieRequest
err = p.Scan(&cookie)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login cookie request fail: %w", err))
}
err = a.WritePacket(pk.Marshal(
packetid.ServerboundLoginCookieResponse,
&server.LoginCookieResponse{Key: cookie.Key},
))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login cookie request fail: %w", err))
}
}
}
}
type OnlineAuthServer struct {
SessionServer string
AuthServer string
}
type OnlineAuth struct {
AccessToken string
Profile Profile
}
func (o *OnlineAuth) Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error {
key, encodeStream, decodeStream := newSymmetricEncryption()
err := o.LoginAuth(ctx, content, key)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("login auth fail: %w", err))
}
// Response with Encryption Key
var pkt pk.Packet
pkt, err = genEncryptionKeyResponse(key, content.PublicKey, content.VerifyToken)
if err != nil {
return fmt.Errorf("gen encryption key response fail: %v", err)
}
err = conn.WritePacket(pkt)
if err != nil {
return err
}
// Set Connection Encryption
conn.SetCipher(encodeStream, decodeStream)
return nil
}
func genEncryptionKeyResponse(shareSecret, publicKey, verifyToken []byte) (erp pk.Packet, err error) {
iPK, err := x509.ParsePKIXPublicKey(publicKey)
if err != nil {
err = fmt.Errorf("decode public key fail: %v", err)
return
}
rsaKey := iPK.(*rsa.PublicKey)
cryptPK, err := rsa.EncryptPKCS1v15(rand.Reader, rsaKey, shareSecret)
if err != nil {
err = fmt.Errorf("encryption share secret fail: %v", err)
return
}
verifyT, err := rsa.EncryptPKCS1v15(rand.Reader, rsaKey, verifyToken)
if err != nil {
err = fmt.Errorf("encryption verfy tokenfail: %v", err)
return erp, err
}
return pk.Marshal(
packetid.ServerboundLoginKey,
pk.ByteArray(cryptPK),
pk.ByteArray(verifyT),
), nil
}
func (o *OnlineAuth) LoginAuth(ctx context.Context, content client.LoginHello, key []byte) error {
digest := authDigest(content.ServerID, key, content.PublicKey)
request, err := json.Marshal(struct {
AccessToken string `json:"accessToken"`
SelectedProfile string `json:"selectedProfile"`
ServerID string `json:"serverId"`
}{
AccessToken: o.AccessToken,
SelectedProfile: hex.EncodeToString(o.Profile.UUID[:]),
ServerID: digest,
})
PostRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://sessionserver.mojang.com/session/minecraft/join",
bytes.NewReader(request))
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("make request fail: %w", err))
}
PostRequest.Header.Set("User-agent", "go-mc")
PostRequest.Header.Set("Connection", "keep-alive")
PostRequest.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(PostRequest)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("session mojang fail: %w", err))
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusNoContent {
return errors.Join(ErrEncrypt, fmt.Errorf("session join fail: %s", string(body)))
}
return nil
}
func newSymmetricEncryption() (key []byte, encoStream, decoStream cipher.Stream) {
key = make([]byte, 16)
if _, err := rand.Read(key); err != nil {
panic(err)
}
b, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
decoStream = CFB8.NewCFB8Decrypt(b, key)
encoStream = CFB8.NewCFB8Encrypt(b, key)
return
}
func (o *OnlineAuth) FetchProfile(ctx context.Context) *Profile {
return &o.Profile
}
func authDigest(serverID string, sharedSecret, publicKey []byte) string {
h := sha1.New()
h.Write([]byte(serverID))
h.Write(sharedSecret)
h.Write(publicKey)
hash := h.Sum(nil)
// Check for negative hashes
negative := (hash[0] & 0x80) == 0x80
if negative {
hash = twosComplement(hash)
}
// Trim away zeroes
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
if negative {
res = "-" + res
}
return res
}
// little endian
func twosComplement(p []byte) []byte {
carry := true
for i := len(p) - 1; i >= 0; i-- {
p[i] = ^p[i]
if carry {
carry = p[i] == 0xff
p[i]++
}
}
return p
}
type OfflineAuth struct {
Username string
}
// NameToUUID return the UUID from player name in offline mode
func NameToUUID(name string) uuid.UUID {
version := 3
h := md5.New()
h.Write([]byte("OfflinePlayer:"))
h.Write([]byte(name))
var id uuid.UUID
h.Sum(id[:0])
id[6] = (id[6] & 0x0f) | uint8((version&0xf)<<4)
id[8] = (id[8] & 0x3f) | 0x80 // RFC 4122 variant
return id
}
func (o *OfflineAuth) FetchProfile(ctx context.Context) *Profile {
return &Profile{
Name: o.Username,
UUID: NameToUUID(o.Username),
}
}
func (o *OfflineAuth) Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error {
return nil
}
type KonjacAuth struct {
*OnlineAuth
UserCode string
Profile Profile
}
func (k *KonjacAuth) LoginAuth(ctx context.Context, content client.LoginHello, key []byte) error {
digest := authDigest(content.ServerID, key, content.PublicKey)
request, err := json.Marshal(struct {
AccessToken string `json:"accessToken"`
SelectedProfile string `json:"selectedProfile"`
ServerID string `json:"serverId"`
}{
AccessToken: k.UserCode,
SelectedProfile: "",
ServerID: digest,
})
PostRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.1:37565/ss/session/minecraft/join",
bytes.NewReader(request))
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("make request fail: %w", err))
}
PostRequest.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(PostRequest)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("session mojang fail: %w", err))
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusNoContent {
return errors.Join(ErrEncrypt, fmt.Errorf("session join fail: %s", string(body)))
}
return nil
}
func (k *KonjacAuth) FetchProfile(ctx context.Context) *Profile {
data, err := json.Marshal(map[string]any{
"agent": map[string]any{
"name": "minego",
"version": 0,
},
"username": k.UserCode,
"password": "",
"clientToken": "",
"requestUser": "",
})
if err != nil {
return nil
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.1:37565/as/authenticate", bytes.NewReader(data))
if err != nil {
return nil
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
_, _ = io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusNoContent {
return nil
}
return nil
}

29
pkg/bot/client.go Normal file
View File

@ -0,0 +1,29 @@
package bot
import (
"context"
"git.konjactw.dev/patyhank/minego/pkg/auth"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/server"
)
type Client interface {
Connect(ctx context.Context, addr string, options *ConnectOptions) error
Close(ctx context.Context) error
IsConnected() bool
WritePacket(ctx context.Context, packet server.ServerboundPacket) error
PacketHandler() PacketHandler
EventHandler() EventHandler
World() World
Inventory() InventoryHandler
Player() Player
}
type ClientOptions struct {
AuthProvider auth.Provider
}
type ConnectOptions struct {
FakeHost string
}

20
pkg/bot/event.go Normal file
View File

@ -0,0 +1,20 @@
package bot
type EventHandler interface {
PublishEvent(event string, data any) error
SubscribeEvent(event string, handler func(data any) error)
}
type Event interface {
EventID() string
}
func PublishEvent(client Client, event Event) error {
return client.EventHandler().PublishEvent(event.EventID(), event)
}
func SubscribeEvent(client Client, event string, handler func(event Event) error) {
client.EventHandler().SubscribeEvent(event, func(data any) error {
return handler(data.(Event))
})
}

24
pkg/bot/handler.go Normal file
View File

@ -0,0 +1,24 @@
package bot
import (
"context"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/client"
"github.com/Tnze/go-mc/data/packetid"
)
type PacketHandler interface {
AddPacketHandler(id packetid.ClientboundPacketID, handler func(ctx context.Context, p client.ClientboundPacket))
AddGenericPacketHandler(handler func(ctx context.Context, p client.ClientboundPacket))
HandlePacket(ctx context.Context, p client.ClientboundPacket)
}
type HandlerFunc[T client.ClientboundPacket] func(ctx context.Context, p T)
func AddHandler[T client.ClientboundPacket](c Client, f HandlerFunc[T]) {
var t T
handler := c.PacketHandler()
handler.AddPacketHandler(t.PacketID(), func(ctx context.Context, p client.ClientboundPacket) {
f(ctx, p.(T))
})
}

23
pkg/bot/inventory.go Normal file
View File

@ -0,0 +1,23 @@
package bot
import (
"git.konjactw.dev/patyhank/minego/pkg/protocol/slot"
"github.com/Tnze/go-mc/level/item"
)
type Container interface {
GetSlot(index int) slot.Slot
Slots() []slot.Slot
SlotCount() int
FindEmpty() int16
FindItem(itemID item.ID) int16
Click(slot int16, mode int32, button int32) error
}
type InventoryHandler interface {
Inventory() Container
Container() Container
CurrentContainerID() int32
Click(container int32, slot int16, mode int32, button int32) error
Close()
}

25
pkg/bot/player.go Normal file
View File

@ -0,0 +1,25 @@
package bot
import (
"git.konjactw.dev/patyhank/minego/pkg/protocol"
"github.com/go-gl/mathgl/mgl64"
)
type Player interface {
StateID() int32
UpdateStateID(id int32)
Entity() Entity
FlyTo(pos mgl64.Vec3) error
WalkTo(pos mgl64.Vec3) error
LookAt(vec3 mgl64.Vec3) error
BreakBlock(pos protocol.Position) error
PlaceBlock(pos protocol.Position) error
PlaceBlockWithArgs(pos protocol.Position, face int32, cursor mgl64.Vec3) error
OpenContainer(pos protocol.Position) (Container, error)
UseItem(hand int8) error
OpenMenu(command string) (Container, error)
}

35
pkg/bot/world.go Normal file
View File

@ -0,0 +1,35 @@
package bot
import (
"git.konjactw.dev/patyhank/minego/pkg/protocol"
"git.konjactw.dev/patyhank/minego/pkg/protocol/metadata"
"git.konjactw.dev/patyhank/minego/pkg/protocol/slot"
"github.com/Tnze/go-mc/data/entity"
"github.com/Tnze/go-mc/level/block"
"github.com/go-gl/mathgl/mgl64"
"github.com/google/uuid"
)
type World interface {
GetBlock(pos protocol.Position) (block.Block, error)
SetBlock(pos protocol.Position, b block.Block) error
GetNearbyBlocks(pos protocol.Position, radius int32) ([]block.Block, error)
FindNearbyBlock(pos protocol.Position, radius int32, blk block.Block) (protocol.Position, error)
Entities() []Entity
GetEntity(id int32) Entity
GetNearbyEntities(radius int32) []Entity
GetEntitiesByType(entityType entity.ID) []Entity
}
type Entity interface {
ID() int32
UUID() uuid.UUID
Type() entity.ID
Position() mgl64.Vec3
Rotation() mgl64.Vec2
Metadata() map[uint8]metadata.Metadata
Equipment() map[int8]slot.Slot
}

180
pkg/client/client.go Normal file
View File

@ -0,0 +1,180 @@
package client
import (
"bytes"
"context"
"errors"
"net"
"strconv"
"git.konjactw.dev/patyhank/minego/pkg/auth"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/game/inventory"
"git.konjactw.dev/patyhank/minego/pkg/game/world"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/client"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/server"
"github.com/Tnze/go-mc/data/packetid"
mcnet "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet"
"golang.org/x/sync/errgroup"
)
type botClient struct {
conn *mcnet.Conn
packetHandler bot.PacketHandler
eventHandler bot.EventHandler
world bot.World
inventory *inventory.Manager
connected bool
authProvider auth.Provider
}
func (b *botClient) Close(ctx context.Context) error {
if err := b.conn.Close(); err != nil {
return err
}
return nil
}
func (b *botClient) IsConnected() bool {
return b.connected
}
func (b *botClient) WritePacket(ctx context.Context, packet server.ServerboundPacket) error {
err := b.conn.WritePacket(pk.Marshal(packet.PacketID(), packet))
if err != nil {
return err
}
return nil
}
func (b *botClient) PacketHandler() bot.PacketHandler {
return b.packetHandler
}
func (b *botClient) EventHandler() bot.EventHandler {
return b.eventHandler
}
func (b *botClient) World() bot.World {
return b.world
}
func (b *botClient) Inventory() bot.InventoryHandler {
return b.inventory
}
func (b *botClient) Connect(ctx context.Context, addr string, options *bot.ConnectOptions) error {
// 套用 go-mc 的連接邏輯
host, portStr, err := net.SplitHostPort(addr)
var port uint64
if err != nil {
var addrErr *net.AddrError
const missingPort = "missing port in address"
if errors.As(err, &addrErr) && addrErr.Err == missingPort {
host = addr
port = 25565
} else {
return err
}
} else {
port, err = strconv.ParseUint(portStr, 0, 16)
if err != nil {
return err
}
}
// 建立連接
dialer := &mcnet.DefaultDialer
conn, err := dialer.DialMCContext(ctx, addr)
if err != nil {
return err
}
// 執行握手
if options != nil && options.FakeHost != "" {
host = options.FakeHost
}
err = b.handshake(conn, host, port)
if err != nil {
return err
}
err = b.login()
if err != nil {
return err
}
err = b.configuration()
if err != nil {
return err
}
b.conn = conn
b.connected = true
// 啟動封包處理 goroutine
go b.handlePackets(ctx)
return nil
}
func (b *botClient) handshake(conn *mcnet.Conn, host string, port uint64) error {
return conn.WritePacket(pk.Marshal(
0,
pk.VarInt(772),
pk.String(host),
pk.UnsignedShort(port),
pk.VarInt(2), // to game state
))
}
func (b *botClient) handlePackets(ctx context.Context) {
group, ctx := errgroup.WithContext(ctx)
group.SetLimit(15)
for {
select {
case <-ctx.Done():
return
default:
var p pk.Packet
if err := b.conn.ReadPacket(&p); err != nil {
return
}
creator, ok := client.ClientboundPackets[packetid.ClientboundPacketID(p.ID)]
if !ok {
continue
}
pkt := creator()
_, err := pkt.ReadFrom(bytes.NewReader(p.Data))
if err != nil {
continue
}
group.Go(func() error {
b.packetHandler.HandlePacket(ctx, pkt)
return nil
})
}
}
}
func NewClient(options *bot.ClientOptions) bot.Client {
c := &botClient{
packetHandler: newPacketHandler(),
authProvider: options.AuthProvider,
}
if options.AuthProvider == nil {
c.authProvider = &auth.OfflineAuth{Username: "Steve"}
}
c.world = world.NewWorld(c)
c.eventHandler = NewEventHandler()
c.inventory = inventory.NewManager(c)
return c
}

74
pkg/client/connect.go Normal file
View File

@ -0,0 +1,74 @@
package client
import (
"context"
"errors"
"time"
"git.konjactw.dev/patyhank/minego/pkg/auth"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
)
func (b *botClient) login() error {
a := &auth.Auth{
Conn: b.conn,
Provider: b.authProvider,
}
ctx, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelFunc()
return a.HandleLogin(ctx)
}
func (b *botClient) configuration() (err error) {
var p pk.Packet
for {
err = b.conn.ReadPacket(&p)
switch packetid.ClientboundPacketID(p.ID) {
case packetid.ClientboundConfigDisconnect:
var reason chat.Message
err = p.Scan(&reason)
if err != nil {
return err
}
return errors.New("kicked: " + reason.String())
case packetid.ClientboundConfigFinishConfiguration:
err = b.conn.WritePacket(pk.Marshal(
packetid.ServerboundConfigFinishConfiguration,
))
return err
case packetid.ClientboundConfigKeepAlive:
var keepAliveID pk.Long
err = p.Scan(&keepAliveID)
if err != nil {
return err
}
err = b.conn.WritePacket(pk.Marshal(packetid.ServerboundConfigKeepAlive, keepAliveID))
if err != nil {
return err
}
case packetid.ClientboundConfigPing:
var pingID pk.Int
err = p.Scan(&pingID)
if err != nil {
return err
}
err = b.conn.WritePacket(pk.Marshal(packetid.ServerboundConfigPong, pingID))
if err != nil {
return err
}
case packetid.ClientboundConfigSelectKnownPacks:
err = b.conn.WritePacket(pk.Marshal(packetid.ServerboundConfigSelectKnownPacks, pk.VarInt(0)))
if err != nil {
return err
}
default:
continue
}
}
}

1
pkg/client/connection.go Normal file
View File

@ -0,0 +1 @@
package client

70
pkg/client/event.go Normal file
View File

@ -0,0 +1,70 @@
package client
import (
"sync"
)
// EventHandler 是一個泛型事件總線
type EventHandler struct {
mu sync.RWMutex
handlers map[string][]func(event any) error
}
func (e *EventHandler) PublishEvent(event string, data any) error {
e.mu.RLock()
defer e.mu.RUnlock()
if hs, ok := e.handlers[event]; ok {
for _, h := range hs {
if err := h(data); err != nil {
return err
}
}
}
return nil
}
func (e *EventHandler) SubscribeEvent(event string, handler func(data any) error) {
e.mu.Lock()
defer e.mu.Unlock()
e.handlers[event] = append(e.handlers[event], handler)
}
func NewEventHandler() *EventHandler {
return &EventHandler{
handlers: make(map[string][]func(any) error),
}
}
//func SubscribeEvent[T bot.Event[T]](eb *EventHandler, t T, f func(event T) error) {
// eb.mu.Lock()
// defer eb.mu.Unlock()
//
// eb.handlers[t.ID()] = append(eb.handlers[t.ID()], func(d any) error {
// t2 := d.(T)
// return f(t2)
// })
//}
//
//func PublishEvent[T bot.Event[T]](eb *EventHandler, t T) error {
// eb.mu.RLock()
// defer eb.mu.RUnlock()
// if hs, ok := eb.handlers[t.ID()]; ok {
// for _, h := range hs {
// if err := h(t); err != nil {
// return err
// }
// }
// }
//
// return nil
//}
//
//func GoPublishEvent[T bot.Event[T]](eb *EventHandler, t T) {
// eb.mu.RLock()
// defer eb.mu.RUnlock()
// if hs, ok := eb.handlers[t.ID()]; ok {
// for _, h := range hs {
// go h(t)
// }
// }
//}

42
pkg/client/handler.go Normal file
View File

@ -0,0 +1,42 @@
package client
import (
"context"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/client"
"github.com/Tnze/go-mc/data/packetid"
)
func newPacketHandler() bot.PacketHandler {
return &packetHandler{
handlerMap: make(map[packetid.ClientboundPacketID][]func(ctx context.Context, p client.ClientboundPacket)),
}
}
type packetHandler struct {
handlerMap map[packetid.ClientboundPacketID][]func(ctx context.Context, p client.ClientboundPacket)
genericMap []func(ctx context.Context, p client.ClientboundPacket)
}
func (ph *packetHandler) AddPacketHandler(id packetid.ClientboundPacketID, handler func(ctx context.Context, p client.ClientboundPacket)) {
f := ph.handlerMap[id]
f = append(f, handler)
ph.handlerMap[id] = f
}
func (ph *packetHandler) AddGenericPacketHandler(handler func(ctx context.Context, p client.ClientboundPacket)) {
ph.genericMap = append(ph.genericMap, handler)
}
func (ph *packetHandler) HandlePacket(ctx context.Context, p client.ClientboundPacket) {
f := ph.handlerMap[p.PacketID()]
if f != nil {
for _, handler := range f {
handler(ctx, p)
}
}
for _, handler := range ph.genericMap {
handler(ctx, p)
}
}

View File

@ -0,0 +1,13 @@
package inventory
import "github.com/Tnze/go-mc/chat"
type ContainerOpenEvent struct {
WindowID int32
Type int32
Title chat.Message
}
func (c ContainerOpenEvent) EventID() string {
return "inventory:container_open"
}

View File

@ -0,0 +1,96 @@
package inventory
import (
"context"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/server"
"git.konjactw.dev/patyhank/minego/pkg/protocol/slot"
"github.com/Tnze/go-mc/level/item"
)
// Container 代表一個容器
type Container struct {
containerID int32
slots []slot.Slot
c bot.Client
}
func NewContainer(c bot.Client, cID int32) *Container {
return &Container{
c: c,
containerID: cID,
slots: make([]slot.Slot, 0),
}
}
func NewContainerWithSize(c bot.Client, cID, size int32) *Container {
return &Container{
c: c,
containerID: cID,
slots: make([]slot.Slot, size),
}
}
func (c *Container) GetSlot(index int) slot.Slot {
if index < 0 || index >= len(c.slots) {
return slot.Slot{}
}
return c.slots[index]
}
func (c *Container) Slots() []slot.Slot {
return c.slots
}
func (c *Container) SlotCount() int {
return len(c.slots)
}
func (c *Container) FindEmpty() int16 {
for i, s := range c.slots {
if s.Count <= 0 {
return int16(i)
}
}
return -1
}
func (c *Container) FindItem(itemID item.ID) int16 {
for i, s := range c.slots {
if s.ItemID == itemID && s.Count > 0 {
return int16(i)
}
}
return -1
}
func (c *Container) SetSlot(index int, s slot.Slot) {
// 自動擴容
for len(c.slots) <= index {
c.slots = append(c.slots, slot.Slot{})
}
if index >= 0 && index < len(c.slots) {
c.slots[index] = s
}
}
func (c *Container) SetSlots(slots []slot.Slot) {
c.slots = make([]slot.Slot, len(slots))
copy(c.slots, slots)
}
func (c *Container) Clear() {
c.slots = make([]slot.Slot, 0)
}
func (c *Container) Click(idx int16, mode int32, button int32) error {
clickPacket := &server.ContainerClick{
WindowID: c.containerID,
StateID: c.c.Player().StateID(),
Slot: idx,
Button: int8(button),
Mode: mode,
}
return c.c.WritePacket(context.Background(), clickPacket)
}

View File

@ -0,0 +1,98 @@
package inventory
import (
"context"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/client"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/server"
"git.konjactw.dev/patyhank/minego/pkg/protocol/slot"
)
// Manager 管理inventory和container
type Manager struct {
c bot.Client
inventory *Container
container *Container
cursor *slot.Slot
currentContainerID int32
}
func NewManager(c bot.Client) *Manager {
m := &Manager{
c: c,
inventory: NewContainerWithSize(c, 0, 45),
currentContainerID: 0,
}
bot.AddHandler(c, func(ctx context.Context, p *client.SetContainerContent) {
if p.WindowID == 0 {
m.inventory.SetSlots(p.Slots)
} else if m.container != nil {
m.container.SetSlots(p.Slots)
}
m.c.Player().UpdateStateID(p.StateID)
})
bot.AddHandler(c, func(ctx context.Context, p *client.ContainerSetSlot) {
if p.ContainerID == 0 {
m.inventory.SetSlot(int(p.Slot), p.ItemStack)
} else if m.container != nil {
m.container.SetSlot(int(p.Slot), p.ItemStack)
}
m.c.Player().UpdateStateID(p.StateID)
})
bot.AddHandler(c, func(ctx context.Context, p *client.CloseContainer) {
if p.WindowID == m.currentContainerID {
m.currentContainerID = -1
if m.container != nil {
m.container = nil
}
}
})
bot.AddHandler(c, func(ctx context.Context, p *client.OpenScreen) {
m.currentContainerID = p.WindowID
m.container = NewContainer(c, p.WindowID)
go bot.PublishEvent(m.c, ContainerOpenEvent{
WindowID: p.WindowID,
Type: p.WindowType,
Title: p.WindowTitle,
})
})
return m
}
func (m *Manager) Inventory() bot.Container {
return m.inventory
}
func (m *Manager) Container() bot.Container {
return m.container
}
func (m *Manager) Cursor() *slot.Slot {
return m.cursor
}
func (m *Manager) CurrentContainerID() int32 {
return m.currentContainerID
}
func (m *Manager) Close() {
if m.currentContainerID != -1 {
_ = m.c.WritePacket(context.Background(), &server.ContainerClose{WindowID: m.currentContainerID})
} else {
_ = m.c.WritePacket(context.Background(), &server.ContainerClose{WindowID: 0})
}
}
// Click 點擊容器slot
func (m *Manager) Click(id int32, slotIndex int16, mode int32, button int32) error {
clickPacket := &server.ContainerClick{
WindowID: id,
StateID: m.c.Player().StateID(),
Slot: slotIndex,
Button: int8(button),
Mode: mode,
}
return m.c.WritePacket(context.Background(), clickPacket)
}

11
pkg/game/player/event.go Normal file
View File

@ -0,0 +1,11 @@
package player
import "github.com/Tnze/go-mc/chat"
type MessageEvent struct {
Message chat.Message
}
func (m MessageEvent) EventID() string {
return "player:message"
}

View File

@ -0,0 +1,188 @@
package player
import (
"container/heap"
"math"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/protocol"
"github.com/Tnze/go-mc/level/block"
"github.com/go-gl/mathgl/mgl64"
)
// Node 表示 A* 演算法中的節點
type Node struct {
Position protocol.Position
G float64 // 從起點到當前節點的實際距離
H float64 // 從當前節點到終點的啟發式距離
F float64 // G + H
Parent *Node
Index int // heap 索引
}
// NodeHeap 實現 heap.Interface 用於優先佇列
type NodeHeap []*Node
func (h NodeHeap) Len() int { return len(h) }
func (h NodeHeap) Less(i, j int) bool { return h[i].F < h[j].F }
func (h NodeHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
h[i].Index = i
h[j].Index = j
}
func (h *NodeHeap) Push(x interface{}) {
n := len(*h)
node := x.(*Node)
node.Index = n
*h = append(*h, node)
}
func (h *NodeHeap) Pop() interface{} {
old := *h
n := len(old)
node := old[n-1]
node.Index = -1
*h = old[0 : n-1]
return node
}
// AStar 使用 A* 演算法尋找路徑
func AStar(world bot.World, start, goal mgl64.Vec3) ([]mgl64.Vec3, error) {
startPos := protocol.Position{int32(start.X()), int32(start.Y()), int32(start.Z())}
goalPos := protocol.Position{int32(goal.X()), int32(goal.Y()), int32(goal.Z())}
openSet := &NodeHeap{}
heap.Init(openSet)
closedSet := make(map[protocol.Position]bool)
allNodes := make(map[protocol.Position]*Node)
startNode := &Node{
Position: startPos,
G: 0,
H: heuristic(startPos, goalPos),
}
startNode.F = startNode.G + startNode.H
heap.Push(openSet, startNode)
allNodes[startPos] = startNode
for openSet.Len() > 0 {
current := heap.Pop(openSet).(*Node)
if current.Position == goalPos {
return reconstructPath(current), nil
}
closedSet[current.Position] = true
// 檢查相鄰節點
for _, neighbor := range getNeighbors(current.Position) {
if closedSet[neighbor] {
continue
}
// 檢查是否可通行
if !isWalkable(world, neighbor) {
continue
}
tentativeG := current.G + distance(current.Position, neighbor)
neighborNode, exists := allNodes[neighbor]
if !exists {
neighborNode = &Node{
Position: neighbor,
G: math.Inf(1),
H: heuristic(neighbor, goalPos),
}
allNodes[neighbor] = neighborNode
}
if tentativeG < neighborNode.G {
neighborNode.Parent = current
neighborNode.G = tentativeG
neighborNode.F = neighborNode.G + neighborNode.H
if neighborNode.Index == -1 {
heap.Push(openSet, neighborNode)
} else {
heap.Fix(openSet, neighborNode.Index)
}
}
}
}
return nil, nil // 找不到路徑
}
// heuristic 計算啟發式距離(曼哈頓距離)
func heuristic(a, b protocol.Position) float64 {
return math.Abs(float64(a[0]-b[0])) + math.Abs(float64(a[1]-b[1])) + math.Abs(float64(a[2]-b[2]))
}
// distance 計算兩點間的實際距離
func distance(a, b protocol.Position) float64 {
dx := float64(a[0] - b[0])
dy := float64(a[1] - b[1])
dz := float64(a[2] - b[2])
return math.Sqrt(dx*dx + dy*dy + dz*dz)
}
// getNeighbors 獲取相鄰節點
func getNeighbors(pos protocol.Position) []protocol.Position {
neighbors := []protocol.Position{
{pos[0] + 1, pos[1], pos[2]}, // 東
{pos[0] - 1, pos[1], pos[2]}, // 西
{pos[0], pos[1], pos[2] + 1}, // 南
{pos[0], pos[1], pos[2] - 1}, // 北
{pos[0], pos[1] + 1, pos[2]}, // 上
{pos[0], pos[1] - 1, pos[2]}, // 下
}
return neighbors
}
// isWalkable 檢查位置是否可通行
func isWalkable(world bot.World, pos protocol.Position) bool {
// 檢查腳部位置
footBlock, err := world.GetBlock(pos)
if err != nil {
return false
}
// 檢查頭部位置
headPos := protocol.Position{pos[0], pos[1] + 1, pos[2]}
headBlock, err := world.GetBlock(headPos)
if err != nil {
return false
}
// 檢查地面位置
groundPos := protocol.Position{pos[0], pos[1] - 1, pos[2]}
groundBlock, err := world.GetBlock(groundPos)
if err != nil {
return false
}
// 腳部和頭部必須是空氣,地面必須是固體方塊
return footBlock == block.Air{} && headBlock == block.Air{} && groundBlock != block.Air{}
}
// reconstructPath 重建路徑
func reconstructPath(node *Node) []mgl64.Vec3 {
var path []mgl64.Vec3
current := node
for current != nil {
pos := mgl64.Vec3{
float64(current.Position[0]),
float64(current.Position[1]),
float64(current.Position[2]),
}
path = append([]mgl64.Vec3{pos}, path...)
current = current.Parent
}
return path
}

323
pkg/game/player/player.go Normal file
View File

@ -0,0 +1,323 @@
package player
import (
"context"
"fmt"
"math"
"time"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/game/world"
"git.konjactw.dev/patyhank/minego/pkg/protocol"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/client"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/server"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/go-gl/mathgl/mgl64"
)
type Player struct {
c bot.Client
entity *world.Entity
stateID int32
lastReceivedPacketTime time.Time
}
// New 創建新的 Player 實例
func New(c bot.Client) *Player {
pl := &Player{
c: c,
entity: &world.Entity{},
}
c.PacketHandler().AddGenericPacketHandler(func(ctx context.Context, pk client.ClientboundPacket) {
pl.lastReceivedPacketTime = time.Now()
})
bot.AddHandler(c, func(ctx context.Context, p *client.SystemChatMessage) {
if !p.Overlay {
bot.PublishEvent(c, MessageEvent{Message: p.Content})
}
})
bot.AddHandler(c, func(ctx context.Context, p *client.PlayerPosition) {
pl.entity.SetPosition(mgl64.Vec3{p.X, p.Y, p.Z})
pl.entity.SetRotation(mgl64.Vec2{float64(p.XRot), float64(p.YRot)})
c.WritePacket(context.Background(), &server.AcceptTeleportation{TeleportID: p.ID})
})
bot.AddHandler(c, func(ctx context.Context, p *client.PlayerRotation) {
pl.entity.SetRotation(mgl64.Vec2{float64(p.Yaw), float64(p.Pitch)})
})
return pl
}
func (p *Player) CheckServer() {
for time.Since(p.lastReceivedPacketTime) > 50*time.Millisecond && p.c.IsConnected() {
time.Sleep(50 * time.Millisecond)
}
}
// StateID 返回當前狀態 ID
func (p *Player) StateID() int32 {
return p.stateID
}
// UpdateStateID 更新狀態 ID
func (p *Player) UpdateStateID(id int32) {
p.stateID = id
}
// Entity 返回玩家實體
func (p *Player) Entity() bot.Entity {
return p.entity
}
// FlyTo 直線飛行到指定位置每5格飛行一段
func (p *Player) FlyTo(pos mgl64.Vec3) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
if p.entity == nil {
return fmt.Errorf("player entity is not initialized")
}
currentPos := p.entity.Position()
direction := pos.Sub(currentPos)
distance := direction.Len()
if distance == 0 {
return nil // 已經在目標位置
}
segmentLength := 8.0
for {
currentPos = p.entity.Position()
direction = pos.Sub(currentPos)
distance = direction.Len()
if distance == 0 {
return nil
}
// 正規化方向向量
direction = direction.Normalize()
moveDistance := math.Min(segmentLength, distance)
target := currentPos.Add(direction.Mul(moveDistance))
if err := p.c.WritePacket(context.Background(), &server.MovePlayerPos{
X: target.X(),
FeetY: target.Y(),
Z: target.Z(),
Flags: 0x00,
}); err != nil {
return fmt.Errorf("failed to move player: %w", err)
}
time.Sleep(50 * time.Millisecond)
}
return nil
}
// WalkTo 使用 A* 演算法步行到指定位置
func (p *Player) WalkTo(pos mgl64.Vec3) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
if p.entity == nil {
return fmt.Errorf("player entity is not initialized")
}
currentPos := p.entity.Position()
// 使用 A* 演算法尋找路徑
path, err := AStar(p.c.World(), currentPos, pos)
if err != nil {
return fmt.Errorf("failed to find path: %w", err)
}
if len(path) == 0 {
return fmt.Errorf("no path found to target position")
}
// 沿著路徑移動
for _, waypoint := range path {
if err := p.c.WritePacket(context.Background(), &server.MovePlayerPos{
X: waypoint.X(),
FeetY: waypoint.Y(),
Z: waypoint.Z(),
Flags: 0x0,
}); err != nil {
return fmt.Errorf("failed to move to waypoint: %w", err)
}
// 短暫延遲以模擬真實移動
time.Sleep(100 * time.Millisecond)
}
return nil
}
// LookAt 看向指定位置
func (p *Player) LookAt(target mgl64.Vec3) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
if p.entity == nil {
return fmt.Errorf("player entity is not initialized")
}
// 計算視角
playerPos := p.entity.Position()
direction := target.Sub(playerPos).Normalize()
// 計算 yaw 和 pitch
yaw := float32(math.Atan2(-direction.X(), direction.Z()) * 180 / math.Pi)
pitch := float32(math.Asin(-direction.Y()) * 180 / math.Pi)
return p.c.WritePacket(context.Background(), &server.MovePlayerRot{
Yaw: yaw,
Pitch: pitch,
Flags: 0x00,
})
}
// BreakBlock 破壞指定位置的方塊
func (p *Player) BreakBlock(pos protocol.Position) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
// 發送開始挖掘封包
startPacket := &server.PlayerAction{
Status: 0,
Sequence: p.stateID,
Location: pk.Position{X: int(pos[0]), Y: int(pos[1]), Z: int(pos[2])},
Face: 1,
}
if err := p.c.WritePacket(context.Background(), startPacket); err != nil {
return fmt.Errorf("failed to send start destroy packet: %w", err)
}
// 發送完成挖掘封包
finishPacket := &server.PlayerAction{
Status: 2,
Sequence: p.stateID,
Location: pk.Position{X: int(pos[0]), Y: int(pos[1]), Z: int(pos[2])},
Face: 1,
}
return p.c.WritePacket(context.Background(), finishPacket)
}
// PlaceBlock 在指定位置放置方塊
func (p *Player) PlaceBlock(pos protocol.Position) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
packet := &server.UseItemOn{
Hand: 0,
Location: pk.Position{X: int(pos[0]), Y: int(pos[1]), Z: int(pos[2])},
Face: 1,
CursorX: 0.5,
CursorY: 0.5,
CursorZ: 0.5,
InsideBlock: false,
Sequence: p.stateID,
}
return p.c.WritePacket(context.Background(), packet)
}
// PlaceBlock 在指定位置放置方塊
func (p *Player) PlaceBlockWithArgs(pos protocol.Position, face int32, cursor mgl64.Vec3) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
packet := &server.UseItemOn{
Hand: 0,
Location: pk.Position{X: int(pos[0]), Y: int(pos[1]), Z: int(pos[2])},
Face: face,
CursorX: float32(cursor[0]),
CursorY: float32(cursor[1]),
CursorZ: float32(cursor[2]),
InsideBlock: false,
Sequence: p.stateID,
}
return p.c.WritePacket(context.Background(), packet)
}
// OpenContainer 打開指定位置的容器
func (p *Player) OpenContainer(pos protocol.Position) (bot.Container, error) {
if p.c == nil {
return nil, fmt.Errorf("client is not initialized")
}
// 發送使用物品封包來打開容器
packet := &server.UseItemOn{
Hand: 1,
Location: pk.Position{X: int(pos[0]), Y: int(pos[1]), Z: int(pos[2])},
Face: 1,
CursorX: 0.5,
CursorY: 0.5,
CursorZ: 0.5,
InsideBlock: false,
WorldBorderHit: false,
Sequence: p.stateID,
}
if err := p.c.WritePacket(context.Background(), packet); err != nil {
return nil, fmt.Errorf("failed to open container: %w", err)
}
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*10)
defer cancelFunc()
for p.c.Inventory().Container() == nil && ctx.Err() == nil {
time.Sleep(time.Millisecond * 50)
}
return p.c.Inventory().Container(), nil
}
// UseItem 使用指定手中的物品
func (p *Player) UseItem(hand int8) error {
if p.c == nil {
return fmt.Errorf("client is not initialized")
}
return p.c.WritePacket(context.Background(), &server.UseItem{
Hand: int32(hand),
Sequence: p.stateID,
Yaw: 0,
Pitch: 0,
})
}
// OpenMenu 打開指定命令的選單
func (p *Player) OpenMenu(command string) (bot.Container, error) {
if p.c == nil {
return nil, fmt.Errorf("client is not initialized")
}
if err := p.c.WritePacket(context.Background(), &server.ChatCommand{
Command: command,
}); err != nil {
return nil, fmt.Errorf("failed to open menu with command '%s': %w", command, err)
}
// 返回客戶端的容器處理器
return p.c.Inventory().Container(), nil
}

59
pkg/game/world/entity.go Normal file
View File

@ -0,0 +1,59 @@
package world
import (
"git.konjactw.dev/patyhank/minego/pkg/protocol/metadata"
"git.konjactw.dev/patyhank/minego/pkg/protocol/slot"
"github.com/Tnze/go-mc/data/entity"
"github.com/go-gl/mathgl/mgl64"
"github.com/google/uuid"
)
type Entity struct {
id int32
entityUUID uuid.UUID
entityType entity.ID
pos mgl64.Vec3
rot mgl64.Vec2
metadata map[uint8]metadata.Metadata
equipment map[int8]slot.Slot
}
func (e *Entity) ID() int32 {
return e.id
}
func (e *Entity) UUID() uuid.UUID {
return e.entityUUID
}
func (e *Entity) Type() entity.ID {
return e.entityType
}
func (e *Entity) Position() mgl64.Vec3 {
return e.pos
}
func (e *Entity) Rotation() mgl64.Vec2 {
return e.rot
}
func (e *Entity) Metadata() map[uint8]metadata.Metadata {
return e.metadata
}
func (e *Entity) Equipment() map[int8]slot.Slot {
return e.equipment
}
func (e *Entity) SetPosition(pos mgl64.Vec3) {
e.pos = pos
}
func (e *Entity) SetRotation(rot mgl64.Vec2) {
e.rot = rot
}
func (e *Entity) SetID(id int32) {
e.id = id
}

19
pkg/game/world/event.go Normal file
View File

@ -0,0 +1,19 @@
package world
import "git.konjactw.dev/patyhank/minego/pkg/bot"
type EntityRemoveEvent struct {
Entity bot.Entity
}
func (e EntityRemoveEvent) EventID() string {
return "world:entity_remove"
}
type EntityAddEvent struct {
EntityID int32
}
func (e EntityAddEvent) EventID() string {
return "world:entity_add"
}

304
pkg/game/world/world.go Normal file
View File

@ -0,0 +1,304 @@
package world
import (
"container/list"
"context"
"errors"
"sync"
"git.konjactw.dev/patyhank/minego/pkg/bot"
"git.konjactw.dev/patyhank/minego/pkg/protocol"
"git.konjactw.dev/patyhank/minego/pkg/protocol/metadata"
cp "git.konjactw.dev/patyhank/minego/pkg/protocol/packet/game/client"
"git.konjactw.dev/patyhank/minego/pkg/protocol/slot"
"github.com/Tnze/go-mc/data/entity"
"github.com/Tnze/go-mc/level"
"github.com/Tnze/go-mc/level/block"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/go-gl/mathgl/mgl64"
"golang.org/x/exp/constraints"
)
type World struct {
c bot.Client
Columns map[level.ChunkPos]*level.Chunk
entities map[int32]*Entity
entityLock sync.Mutex
chunkLock sync.Mutex
}
func NewWorld(c bot.Client) *World {
w := &World{
c: c,
Columns: make(map[level.ChunkPos]*level.Chunk),
}
bot.AddHandler(c, func(ctx context.Context, p *cp.LevelChunkWithLight) {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
w.Columns[p.Pos] = p.Data
})
bot.AddHandler(c, func(ctx context.Context, p *cp.ForgetLevelChunk) {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
delete(w.Columns, p.Pos)
})
bot.AddHandler(c, func(ctx context.Context, p *cp.Respawn) {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
w.Columns = make(map[level.ChunkPos]*level.Chunk)
})
bot.AddHandler(c, func(ctx context.Context, p *cp.AddEntity) {
w.entities[p.ID] = &Entity{
id: p.ID,
entityUUID: p.UUID,
entityType: entity.ID(p.Type),
pos: mgl64.Vec3{p.X, p.Y, p.Z},
rot: mgl64.Vec2{pk.Angle(p.XRot).ToDeg(), pk.Angle(p.YRot).ToDeg()},
metadata: nil,
equipment: nil,
}
})
bot.AddHandler(c, func(ctx context.Context, p *cp.RemoveEntities) {
w.entityLock.Lock()
defer w.entityLock.Unlock()
for _, d := range p.EntityIDs {
e, ok := w.entities[d]
if ok {
bot.PublishEvent(c, EntityRemoveEvent{Entity: e})
delete(w.entities, d)
}
}
})
bot.AddHandler(c, func(ctx context.Context, p *cp.SetEntityMetadata) {
w.entityLock.Lock()
defer w.entityLock.Unlock()
e, ok := w.entities[p.EntityID]
if ok {
if e.metadata == nil {
e.metadata = make(map[uint8]metadata.Metadata)
}
for u, entityMetadata := range p.Metadata.Data {
e.metadata[u] = entityMetadata
}
}
})
bot.AddHandler(c, func(ctx context.Context, p *cp.SetEquipment) {
w.entityLock.Lock()
defer w.entityLock.Unlock()
e, ok := w.entities[p.EntityID]
if ok {
if e.equipment == nil {
e.equipment = make(map[int8]slot.Slot)
}
for _, equipment := range p.Equipment {
e.equipment[equipment.Slot] = equipment.Item
}
}
})
bot.AddHandler(c, func(ctx context.Context, p *cp.UpdateEntityPosition) {
w.entityLock.Lock()
defer w.entityLock.Unlock()
if e, ok := w.entities[p.EntityID]; ok {
e.pos = e.pos.Add(mgl64.Vec3{float64(p.DeltaX) / 4096.0, float64(p.DeltaY) / 4096.0, float64(p.DeltaZ) / 4096.0})
}
})
bot.AddHandler(c, func(ctx context.Context, p *cp.UpdateEntityRotation) {
w.entityLock.Lock()
defer w.entityLock.Unlock()
if e, ok := w.entities[p.EntityID]; ok {
e.rot = mgl64.Vec2{float64(p.Yaw), float64(p.Pitch)}
}
})
bot.AddHandler(c, func(ctx context.Context, p *cp.UpdateEntityPositionAndRotation) {
w.entityLock.Lock()
defer w.entityLock.Unlock()
if e, ok := w.entities[p.EntityID]; ok {
e.pos = e.pos.Add(mgl64.Vec3{float64(p.DeltaX) / 4096.0, float64(p.DeltaY) / 4096.0, float64(p.DeltaZ) / 4096.0})
}
})
return w
}
func (w *World) GetBlock(pos protocol.Position) (block.Block, error) {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
chunkX := pos[0] >> 4
chunkZ := pos[2] >> 4
pos2d := level.ChunkPos{chunkX, chunkZ}
chunk, ok := w.Columns[pos2d]
if !ok {
return nil, errors.New("chunk not loaded")
}
blockX := pos[0] & 15
blockZ := pos[2] & 15
blockIdx := (pos[1] << 8) | (blockZ << 4) | blockX
sectionY := pos[1] >> 4
if sectionY < 0 || int(sectionY) >= len(chunk.Sections) {
return nil, errors.New("invalid section Y coordinate")
}
blockStateId := chunk.Sections[sectionY].GetBlock(int(blockIdx))
return block.StateList[blockStateId], nil
}
func (w *World) SetBlock(pos protocol.Position, blk block.Block) error {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
chunkX := pos[0] >> 4
chunkZ := pos[2] >> 4
pos2d := level.ChunkPos{chunkX, chunkZ}
chunk, ok := w.Columns[pos2d]
if !ok {
return errors.New("chunk not loaded")
}
blockX := pos[0] & 15
blockZ := pos[2] & 15
sectionY := pos[1] >> 4
blockY := pos[1] & 15
if sectionY < 0 || int(sectionY) >= len(chunk.Sections) {
return errors.New("invalid section Y coordinate")
}
section := chunk.Sections[sectionY]
blockIdx := (blockY << 8) | (blockZ << 4) | blockX
section.SetBlock(int(blockIdx), block.ToStateID[blk])
return nil
}
func (w *World) GetNearbyBlocks(pos protocol.Position, radius int32) ([]block.Block, error) {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
var blocks []block.Block
for dx := -radius; dx <= radius; dx++ {
for dy := -radius; dy <= radius; dy++ {
for dz := -radius; dz <= radius; dz++ {
blk, err := w.GetBlock(protocol.Position{pos[0] + dx, pos[1] + dy, pos[2] + dz})
if err != nil {
continue
}
blocks = append(blocks, blk)
}
}
}
return blocks, nil
}
func (w *World) FindNearbyBlock(pos protocol.Position, radius int32, blk block.Block) (protocol.Position, error) {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
visited := make(map[protocol.Position]bool)
queue := list.New()
start := pos
queue.PushBack(start)
visited[start] = true
// Direction vectors for 6-way adjacent blocks
dirs := []protocol.Position{
{1, 0, 0}, {-1, 0, 0},
{0, 1, 0}, {0, -1, 0},
{0, 0, 1}, {0, 0, -1},
}
for queue.Len() > 0 {
current := queue.Remove(queue.Front()).(protocol.Position)
// Skip if beyond the radius
if abs(current[0]-pos[0]) > radius || abs(current[1]-pos[1]) > radius || abs(current[2]-pos[2]) > radius {
continue
}
// Check if current block matches target
if currentBlock, err := w.GetBlock(current); err == nil {
if currentBlock == blk {
return current, nil
}
}
// Check all 6 adjacent blocks
for _, dir := range dirs {
next := protocol.Position{
current[0] + dir[0],
current[1] + dir[1],
current[2] + dir[2],
}
if !visited[next] {
visited[next] = true
queue.PushBack(next)
}
}
}
return protocol.Position{}, errors.New("block not found")
}
func (w *World) Entities() []bot.Entity {
w.entityLock.Lock()
defer w.entityLock.Unlock()
var entities []bot.Entity
for _, e := range w.entities {
entities = append(entities, e)
}
return entities
}
func (w *World) GetEntity(id int32) bot.Entity {
w.entityLock.Lock()
defer w.entityLock.Unlock()
return w.entities[id]
}
func (w *World) GetNearbyEntities(radius int32) []bot.Entity {
w.entityLock.Lock()
defer w.entityLock.Unlock()
selfPos := w.c.Player().Entity().Position()
var entities []bot.Entity
for _, e := range w.entities {
sqr := e.pos.Sub(selfPos).LenSqr()
if sqr <= float64(radius*radius) {
entities = append(entities, e)
}
}
return entities
}
func (w *World) GetEntitiesByType(entityType entity.ID) []bot.Entity {
w.entityLock.Lock()
defer w.entityLock.Unlock()
var entities []bot.Entity
for _, e := range w.entities {
if e.entityType == entityType {
entities = append(entities, e)
}
}
return entities
}
func abs[T constraints.Signed | constraints.Float](x T) T {
if x < 0 {
return -x
}
return x
}

View File

@ -3698,102 +3698,6 @@ func (a *BoolVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
return n, err
}
// Int32VarIntArray a utility type for encoding/decoding packet.Int -> int32[packet.VarInt] slice.
type Int32VarIntArray []int32
func (a Int32VarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Int(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int32VarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int32VarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Int)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int32PrefixedArrayVarIntArray a utility type for encoding/decoding packet.Int -> int32[packet.VarInt] slice.
type Int32PrefixedArrayVarIntArray []int32
func (a Int32PrefixedArrayVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Int(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int32PrefixedArrayVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int32PrefixedArrayVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Int)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// StringVarIntArray a utility type for encoding/decoding packet.String -> string[packet.VarInt] slice.
type StringVarIntArray []string
@ -3889,3 +3793,99 @@ func (a *Int32VarIntVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
return n, err
}
// Int32VarIntArray a utility type for encoding/decoding packet.Int -> int32[packet.VarInt] slice.
type Int32VarIntArray []int32
func (a Int32VarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Int(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int32VarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int32VarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Int)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int32PrefixedArrayVarIntArray a utility type for encoding/decoding packet.Int -> int32[packet.VarInt] slice.
type Int32PrefixedArrayVarIntArray []int32
func (a Int32PrefixedArrayVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Int(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int32PrefixedArrayVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int32PrefixedArrayVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Int)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}

View File

@ -46,13 +46,13 @@ const (
MetadataQuaternion
)
type entityMetadata interface {
type Metadata interface {
EntityMetadataType() MetadataType
pk.Field
}
type EntityMetadata struct {
Data map[uint8]entityMetadata
Data map[uint8]Metadata
}
func (m EntityMetadata) WriteTo(w io.Writer) (int64, error) {
@ -114,44 +114,44 @@ func (m *EntityMetadata) ReadFrom(r io.Reader) (int64, error) {
return n, nil
}
type metadataCreator func() entityMetadata
type metadataCreator func() Metadata
var metadataType = map[MetadataType]metadataCreator{}
func init() {
metadataType[MetadataByte] = func() entityMetadata { return &Byte{} }
metadataType[MetadataVarInt] = func() entityMetadata { return &VarInt{} }
metadataType[MetadataVarLong] = func() entityMetadata { return &VarLong{} }
metadataType[MetadataFloat] = func() entityMetadata { return &Float{} }
metadataType[MetadataString] = func() entityMetadata { return &String{} }
metadataType[MetadataChat] = func() entityMetadata { return &Chat{} }
metadataType[MetadataOptChat] = func() entityMetadata { return &OptChat{} }
metadataType[MetadataSlot] = func() entityMetadata { return &Slot{} }
metadataType[MetadataBoolean] = func() entityMetadata { return &Boolean{} }
metadataType[MetadataRotation] = func() entityMetadata { return &Rotation{} }
metadataType[MetadataPosition] = func() entityMetadata { return &Position{} }
metadataType[MetadataOptPosition] = func() entityMetadata { return &OptPosition{} }
metadataType[MetadataDirection] = func() entityMetadata { return &Direction{} }
metadataType[MetadataOptLivingEntity] = func() entityMetadata { return &OptLivingEntity{} }
metadataType[MetadataBlockState] = func() entityMetadata { return &BlockState{} }
metadataType[MetadataOptBlockState] = func() entityMetadata { return &OptBlockState{} }
metadataType[MetadataNBT] = func() entityMetadata { return &NBT{} }
metadataType[MetadataParticle] = func() entityMetadata { return &Particle{} }
metadataType[MetadataParticles] = func() entityMetadata { return &Particles{} }
metadataType[MetadataVillagerData] = func() entityMetadata { return &VillagerData{} }
metadataType[MetadataOptVarInt] = func() entityMetadata { return &OptVarInt{} }
metadataType[MetadataPose] = func() entityMetadata { return &Pose{} }
metadataType[MetadataCatVariant] = func() entityMetadata { return &CatVariant{} }
metadataType[MetadataCowVariant] = func() entityMetadata { return &CowVariant{} }
metadataType[MetadataWolfVariant] = func() entityMetadata { return &WolfVariant{} }
metadataType[MetadataWolfSoundVariant] = func() entityMetadata { return &WolfSoundVariant{} }
metadataType[MetadataFrogVariant] = func() entityMetadata { return &FrogVariant{} }
metadataType[MetadataPigVariant] = func() entityMetadata { return &PigVariant{} }
metadataType[MetadataChickenVariant] = func() entityMetadata { return &ChickenVariant{} }
metadataType[MetadataOptGlobalPosition] = func() entityMetadata { return &OptGlobalPosition{} }
metadataType[MetadataPaintingVariant] = func() entityMetadata { return &PaintingVariant{} }
metadataType[MetadataSnifferVariant] = func() entityMetadata { return &SnifferVariant{} }
metadataType[MetadataArmadilloState] = func() entityMetadata { return &ArmadilloState{} }
metadataType[MetadataVector3] = func() entityMetadata { return &Vector3{} }
metadataType[MetadataQuaternion] = func() entityMetadata { return &Quaternion{} }
metadataType[MetadataByte] = func() Metadata { return &Byte{} }
metadataType[MetadataVarInt] = func() Metadata { return &VarInt{} }
metadataType[MetadataVarLong] = func() Metadata { return &VarLong{} }
metadataType[MetadataFloat] = func() Metadata { return &Float{} }
metadataType[MetadataString] = func() Metadata { return &String{} }
metadataType[MetadataChat] = func() Metadata { return &Chat{} }
metadataType[MetadataOptChat] = func() Metadata { return &OptChat{} }
metadataType[MetadataSlot] = func() Metadata { return &Slot{} }
metadataType[MetadataBoolean] = func() Metadata { return &Boolean{} }
metadataType[MetadataRotation] = func() Metadata { return &Rotation{} }
metadataType[MetadataPosition] = func() Metadata { return &Position{} }
metadataType[MetadataOptPosition] = func() Metadata { return &OptPosition{} }
metadataType[MetadataDirection] = func() Metadata { return &Direction{} }
metadataType[MetadataOptLivingEntity] = func() Metadata { return &OptLivingEntity{} }
metadataType[MetadataBlockState] = func() Metadata { return &BlockState{} }
metadataType[MetadataOptBlockState] = func() Metadata { return &OptBlockState{} }
metadataType[MetadataNBT] = func() Metadata { return &NBT{} }
metadataType[MetadataParticle] = func() Metadata { return &Particle{} }
metadataType[MetadataParticles] = func() Metadata { return &Particles{} }
metadataType[MetadataVillagerData] = func() Metadata { return &VillagerData{} }
metadataType[MetadataOptVarInt] = func() Metadata { return &OptVarInt{} }
metadataType[MetadataPose] = func() Metadata { return &Pose{} }
metadataType[MetadataCatVariant] = func() Metadata { return &CatVariant{} }
metadataType[MetadataCowVariant] = func() Metadata { return &CowVariant{} }
metadataType[MetadataWolfVariant] = func() Metadata { return &WolfVariant{} }
metadataType[MetadataWolfSoundVariant] = func() Metadata { return &WolfSoundVariant{} }
metadataType[MetadataFrogVariant] = func() Metadata { return &FrogVariant{} }
metadataType[MetadataPigVariant] = func() Metadata { return &PigVariant{} }
metadataType[MetadataChickenVariant] = func() Metadata { return &ChickenVariant{} }
metadataType[MetadataOptGlobalPosition] = func() Metadata { return &OptGlobalPosition{} }
metadataType[MetadataPaintingVariant] = func() Metadata { return &PaintingVariant{} }
metadataType[MetadataSnifferVariant] = func() Metadata { return &SnifferVariant{} }
metadataType[MetadataArmadilloState] = func() Metadata { return &ArmadilloState{} }
metadataType[MetadataVector3] = func() Metadata { return &Vector3{} }
metadataType[MetadataQuaternion] = func() Metadata { return &Quaternion{} }
}

View File

@ -343,54 +343,6 @@ func (c ConfigUpdateEnabledFeatures) WriteTo(w io.Writer) (n int64, err error) {
return n, err
}
// StringIdentifierVarIntArray a utility type for encoding/decoding packet.Identifier -> string[packet.VarInt] slice.
type StringIdentifierVarIntArray []string
func (a StringIdentifierVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Identifier(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *StringIdentifierVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(StringIdentifierVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Identifier)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int8VarIntArray a utility type for encoding/decoding packet.Byte -> int8[packet.VarInt] slice.
type Int8VarIntArray []int8
@ -438,3 +390,51 @@ func (a *Int8VarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
return n, err
}
// StringIdentifierVarIntArray a utility type for encoding/decoding packet.Identifier -> string[packet.VarInt] slice.
type StringIdentifierVarIntArray []string
func (a StringIdentifierVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Identifier(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *StringIdentifierVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(StringIdentifierVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Identifier)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}

View File

@ -1557,35 +1557,7 @@ func (c Explode) WriteTo(w io.Writer) (n int64, err error) {
}
return n, err
}
func (c *ChunkPos) ReadFrom(r io.Reader) (n int64, err error) {
var temp int64
temp, err = (*packet.Int)(&c.X).ReadFrom(r)
n += temp
if err != nil {
return n, err
}
temp, err = (*packet.Int)(&c.Z).ReadFrom(r)
n += temp
if err != nil {
return n, err
}
return n, err
}
func (c ChunkPos) WriteTo(w io.Writer) (n int64, err error) {
var temp int64
temp, err = (*packet.Int)(&c.X).WriteTo(w)
n += temp
if err != nil {
return n, err
}
temp, err = (*packet.Int)(&c.Z).WriteTo(w)
n += temp
if err != nil {
return n, err
}
return n, err
}
func (c *ForgetLevelChunk) ReadFrom(r io.Reader) (n int64, err error) {
var temp int64
temp, err = (&c.Pos).ReadFrom(r)
@ -1810,45 +1782,6 @@ func (c KeepAlive) WriteTo(w io.Writer) (n int64, err error) {
}
return n, err
}
func (c *LevelChunkWithLight) ReadFrom(r io.Reader) (n int64, err error) {
var temp int64
temp, err = (*packet.Int)(&c.X).ReadFrom(r)
n += temp
if err != nil {
return n, err
}
temp, err = (*packet.Int)(&c.Z).ReadFrom(r)
n += temp
if err != nil {
return n, err
}
temp, err = (&c.Data).ReadFrom(r)
n += temp
if err != nil {
return n, err
}
return n, err
}
func (c LevelChunkWithLight) WriteTo(w io.Writer) (n int64, err error) {
var temp int64
temp, err = (*packet.Int)(&c.X).WriteTo(w)
n += temp
if err != nil {
return n, err
}
temp, err = (*packet.Int)(&c.Z).WriteTo(w)
n += temp
if err != nil {
return n, err
}
temp, err = (&c.Data).WriteTo(w)
n += temp
if err != nil {
return n, err
}
return n, err
}
func (c *LevelEvent) ReadFrom(r io.Reader) (n int64, err error) {
var temp int64
temp, err = (*packet.Int)(&c.Type).ReadFrom(r)
@ -7157,102 +7090,6 @@ func (c Waypoint) WriteTo(w io.Writer) (n int64, err error) {
return n, err
}
// StringIdentifierVarIntArray a utility type for encoding/decoding packet.Identifier -> string[packet.VarInt] slice.
type StringIdentifierVarIntArray []string
func (a StringIdentifierVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Identifier(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *StringIdentifierVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(StringIdentifierVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Identifier)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int32VarIntVarIntArray a utility type for encoding/decoding packet.VarInt -> int32[packet.VarInt] slice.
type Int32VarIntVarIntArray []int32
func (a Int32VarIntVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.VarInt(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int32VarIntVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int32VarIntVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.VarInt)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// StringVarIntArray a utility type for encoding/decoding packet.String -> string[packet.VarInt] slice.
type StringVarIntArray []string
@ -7349,6 +7186,54 @@ func (a *UuidUUIDUUIDVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
return n, err
}
// Int32VarIntVarIntArray a utility type for encoding/decoding packet.VarInt -> int32[packet.VarInt] slice.
type Int32VarIntVarIntArray []int32
func (a Int32VarIntVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.VarInt(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int32VarIntVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int32VarIntVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.VarInt)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int64VarLongVarIntArray a utility type for encoding/decoding packet.VarLong -> int64[packet.VarInt] slice.
type Int64VarLongVarIntArray []int64
@ -7397,102 +7282,6 @@ func (a *Int64VarLongVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
return n, err
}
// Int8ByteVarIntArray a utility type for encoding/decoding packet.Byte -> int8[packet.VarInt] slice.
type Int8ByteVarIntArray []int8
func (a Int8ByteVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Byte(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int8ByteVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int8ByteVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Byte)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// StringStringVarIntArray a utility type for encoding/decoding packet.String -> string[packet.VarInt] slice.
type StringStringVarIntArray []string
func (a StringStringVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.String(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *StringStringVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(StringStringVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.String)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int8VarIntArray a utility type for encoding/decoding packet.Byte -> int8[packet.VarInt] slice.
type Int8VarIntArray []int8
@ -7588,3 +7377,147 @@ func (a *Int64VarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
return n, err
}
// StringIdentifierVarIntArray a utility type for encoding/decoding packet.Identifier -> string[packet.VarInt] slice.
type StringIdentifierVarIntArray []string
func (a StringIdentifierVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Identifier(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *StringIdentifierVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(StringIdentifierVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Identifier)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// Int8ByteVarIntArray a utility type for encoding/decoding packet.Byte -> int8[packet.VarInt] slice.
type Int8ByteVarIntArray []int8
func (a Int8ByteVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.Byte(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *Int8ByteVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(Int8ByteVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.Byte)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}
// StringStringVarIntArray a utility type for encoding/decoding packet.String -> string[packet.VarInt] slice.
type StringStringVarIntArray []string
func (a StringStringVarIntArray) WriteTo(w io.Writer) (n int64, err error) {
size := len(a)
nn, err := packet.VarInt(size).WriteTo(w)
if err != nil {
return n, err
}
n += nn
for i := 0; i < size; i++ {
nn, err := packet.String(a[i]).WriteTo(w)
n += nn
if err != nil {
return n, err
}
}
return n, nil
}
func (a *StringStringVarIntArray) ReadFrom(r io.Reader) (n int64, err error) {
var size packet.VarInt
nn, err := size.ReadFrom(r)
n += nn
if err != nil {
return n, err
}
if size < 0 {
return n, errors.New("array length less than zero")
}
if cap(*a) >= int(size) {
*a = (*a)[:int(size)]
} else {
*a = make(StringStringVarIntArray, int(size))
}
for i := 0; i < int(size); i++ {
nn, err = (*packet.String)(&(*a)[i]).ReadFrom(r)
n += nn
if err != nil {
return n, err
}
}
return n, err
}

View File

@ -2,18 +2,14 @@ package client
import (
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/level"
)
var _ ClientboundPacket = (*ForgetLevelChunk)(nil)
//codec:gen
type ChunkPos struct {
X, Z int32
}
//codec:gen
type ForgetLevelChunk struct {
Pos ChunkPos
Pos level.ChunkPos
}
func (ForgetLevelChunk) ClientboundPacketID() packetid.ClientboundPacketID {

View File

@ -1,12 +1,45 @@
package client
import "github.com/Tnze/go-mc/level"
import (
"io"
"github.com/Tnze/go-mc/level"
)
var _ ClientboundPacket = (*LevelChunkWithLight)(nil)
//codec:gen
type LevelChunkWithLight struct {
X int32
Z int32
Data level.Chunk
Pos level.ChunkPos
Data *level.Chunk
}
func (c *LevelChunkWithLight) ReadFrom(r io.Reader) (n int64, err error) {
temp, err := c.Pos.ReadFrom(r)
if err != nil {
return temp, err
}
c.Data = level.EmptyChunk(36)
temp, err = (c.Data).ReadFrom(r)
n += temp
if err != nil {
return n, err
}
return n, err
}
func (c LevelChunkWithLight) WriteTo(w io.Writer) (n int64, err error) {
var temp int64
temp, err = c.Pos.WriteTo(w)
n += temp
if err != nil {
return n, err
}
temp, err = (*level.Chunk)(c.Data).WriteTo(w)
n += temp
if err != nil {
return n, err
}
return n, err
}

51
pkg/protocol/position.go Normal file
View File

@ -0,0 +1,51 @@
package protocol
import "math"
type Position [3]int32
func (p Position) DistanceTo(other Position) float64 {
dx := float64(p[0] - other[0])
dy := float64(p[1] - other[1])
dz := float64(p[2] - other[2])
return math.Sqrt(dx*dx + dy*dy + dz*dz)
}
func (p Position) DistanceToSquared(other Position) float64 {
dx := float64(p[0] - other[0])
dy := float64(p[1] - other[1])
dz := float64(p[2] - other[2])
return dx*dx + dy*dy + dz*dz
}
func (p Position) Add(other Position) Position {
return Position{p[0] + other[0], p[1] + other[1], p[2] + other[2]}
}
func (p Position) Sub(other Position) Position {
return Position{p[0] - other[0], p[1] - other[1], p[2] - other[2]}
}
func (p Position) Mul(scalar float64) Position {
return Position{int32(float64(p[0]) * scalar), int32(float64(p[1]) * scalar), int32(float64(p[2]) * scalar)}
}
func (p Position) Div(scalar float64) Position {
return Position{int32(float64(p[0]) / scalar), int32(float64(p[1]) / scalar), int32(float64(p[2]) / scalar)}
}
func (p Position) IsZero() bool {
return p[0] == 0 && p[1] == 0 && p[2] == 0
}
func (p Position) Clone() Position {
return Position{p[0], p[1], p[2]}
}
func (p Position) String() string {
return "(" + string(p[0]) + ", " + string(p[1]) + ", " + string(p[2]) + ")"
}
func (p Position) Equals(other Position) bool {
return p[0] == other[0] && p[1] == other[1] && p[2] == other[2]
}

View File

@ -3,12 +3,13 @@ package slot
import (
"io"
"github.com/Tnze/go-mc/level/item"
pk "github.com/Tnze/go-mc/net/packet"
)
type Slot struct {
Count int32
ItemID int32
ItemID item.ID
AddComponent []Component
RemoveComponent []ComponentID
}
@ -64,12 +65,16 @@ func (s *Slot) ReadFrom(r io.Reader) (n int64, err error) {
return temp, err
}
n += temp
temp, err = (*pk.VarInt)(&s.ItemID).ReadFrom(r)
var itemID int32
temp, err = (*pk.VarInt)(&itemID).ReadFrom(r)
n += temp
if err != nil {
return temp, err
}
s.ItemID = item.ID(itemID)
addLens := int32(0)
temp, err = (*pk.VarInt)(&addLens).ReadFrom(r)
n += temp

118
readme.md
View File

@ -1,3 +1,117 @@
## 說明
# minego
Minecraft 1.21.6 go-mc server and client
go-mc with command-line-only bot client.
# 建議目錄結構
```
minego/
├─ cmd/
│ ├─ minectl/ # 範例 CLI連線、發包、抓封包
│ │ └─ main.go
│ └─ proxy/ # 範例:簡易協議代理/抓包器
│ └─ main.go
├─ pkg/ # 對外公開 API
│ ├─ client/ # 高階 Client SDK使用者只需要這個
│ │ ├─ client.go # Client 對外介面、New(...)、Connect(...)
│ │ ├─ options.go # 可選項:代理、壓縮、密碼學、登入方式
│ │ ├─ session.go # 與伺服器的一次連線(狀態機)
│ │ ├─ pipeline.go # 封包處理管線decode -> route -> handler
│ │ ├─ dispatcher.go # 事件/封包分發與訂閱
│ │ ├─ keepalive.go
│ │ ├─ reconnect.go
│ │ └─ errors.go
│ │
│ ├─ transport/ # 可替換傳輸層TCP wrapper
│ │ ├─ tcp/
│ │ │ └─ conn.go
│ │ └─ transport.go # 抽象介面Dial(ctx, addr) (Conn, error)
│ │
│ ├─ auth/ # 登入/加密/密鑰交換mojang、離線、自訂yggdrasil
│ │ ├─ offline.go
│ │ ├─ mojang.go
│ │ └─ encrypt.go
│ │
│ ├─ handler/ # 封包與事件處理(基於協議的 client 方向)
│ │ ├─ login.go
│ │ ├─ play_entities.go
│ │ ├─ play_world.go
│ │ ├─ chat.go
│ │ └─ registry.go # 封包 -> handler 的綁定註冊
│ │
│ ├─ game/ # 遊戲狀態(抽象,不與 GUI 綁死)
│ │ ├─ world/
│ │ │ ├─ chunk.go
│ │ │ ├─ palette.go
│ │ │ └─ biome.go
│ │ ├─ entity/
│ │ │ └─ entity.go
│ │ └─ inventory/
│ │ └─ slots.go
│ │
│ ├─ data/ # 協議資料&對照版本表、映射、assets
│ │ ├─ versions/
│ │ │ └─ 1_21.json
│ │ └─ registries/
│ │ └─ packets.json
│ │
│ ├─ protocol/ # 你現有的 codec/packet/metadata 可移到這
│ │ ├─ codec/…
│ │ ├─ packet/…
│ │ └─ nbt/…
│ │
│ └─ util/ # 小工具varint、zlib、pool、log
│ └─ …
└─ go.mod
```
> 原則:
>
> * `pkg/` 對外公開、穩定 API`internal/` 只給本專案使用。
> * `protocol` 保持「與傳輸無關」;`transport` 抽象連線;`client` 串起狀態機與 handler。
> * `handler` 專心處理「已解碼封包」到「遊戲狀態/事件」的映射。
> * `game` 做資料模型(世界、實體、物品),不要直接依賴 UI。
---
# 模組邏輯切分(設計重點)
1. **狀態機State Machine**
* `Handshake -> Status/Login -> Play -> (Disconnected)`
*`session.go` 以 goroutine + channel 管理讀寫,使用 `context.Context` 控制生命週期。
2. **封包管線Pipeline**
* `reader` 取得原始 bytes → `protocol/codec` 解碼 → `dispatcher` 依封包 ID 分派到 handler。
* 可在管線節點插中介:壓縮、加密、記錄、度量。
3. **事件導向 API**
* 對外提供:
```go
type Client interface {
On(event Event, fn any) Unsub
Send(ctx context.Context, p protocol.Packet) error
State() client.State
}
```
* `On(PacketPlayChat, func(*ChatMessage){…})` 這種型別安全的註冊可以用泛型或介面實作。
4. **錯誤與可觀測性**
* 統一 `errors.go`;在 pipeline/handler 位置加可選的 `WithLogger / WithMetrics`。
---
# 命名與風格小訣竅
* 套件名短小、名詞為主:`client`, `auth`, `transport`, `handler`, `game`, `protocol`.
* 檔名以職責分組,不以每個封包獨立檔案(容易爆量難找)。
* 對外只匯出 `pkg/client` 的型別;其餘盡量小寫封裝。
* 盡量用 `context.Context`、`io.Reader/Writer` 介面做邊界。
---
> MCProtocol(新wiki.vg) https://minecraft.wiki/w/Java_Edition_protocol
> 使用套件: github.com/Tnze/go-mc