diff --git a/go.mod b/go.mod index 101b9fc..43803c7 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 13b0420..ec708b4 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 0000000..0d02d98 --- /dev/null +++ b/pkg/auth/auth.go @@ -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 +} diff --git a/pkg/bot/client.go b/pkg/bot/client.go new file mode 100644 index 0000000..41cd367 --- /dev/null +++ b/pkg/bot/client.go @@ -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 +} diff --git a/pkg/bot/event.go b/pkg/bot/event.go new file mode 100644 index 0000000..3b23d45 --- /dev/null +++ b/pkg/bot/event.go @@ -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)) + }) +} diff --git a/pkg/bot/handler.go b/pkg/bot/handler.go new file mode 100644 index 0000000..c1d5c02 --- /dev/null +++ b/pkg/bot/handler.go @@ -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)) + }) +} diff --git a/pkg/bot/inventory.go b/pkg/bot/inventory.go new file mode 100644 index 0000000..6e46cf6 --- /dev/null +++ b/pkg/bot/inventory.go @@ -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() +} diff --git a/pkg/bot/player.go b/pkg/bot/player.go new file mode 100644 index 0000000..05eb34f --- /dev/null +++ b/pkg/bot/player.go @@ -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) +} diff --git a/pkg/bot/world.go b/pkg/bot/world.go new file mode 100644 index 0000000..1e8a87f --- /dev/null +++ b/pkg/bot/world.go @@ -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 +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..ac33519 --- /dev/null +++ b/pkg/client/client.go @@ -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 +} diff --git a/pkg/client/connect.go b/pkg/client/connect.go new file mode 100644 index 0000000..f9914dd --- /dev/null +++ b/pkg/client/connect.go @@ -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 + } + } +} diff --git a/pkg/client/connection.go b/pkg/client/connection.go new file mode 100644 index 0000000..da13c8e --- /dev/null +++ b/pkg/client/connection.go @@ -0,0 +1 @@ +package client diff --git a/pkg/client/event.go b/pkg/client/event.go new file mode 100644 index 0000000..194aa9a --- /dev/null +++ b/pkg/client/event.go @@ -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) +// } +// } +//} diff --git a/pkg/client/handler.go b/pkg/client/handler.go new file mode 100644 index 0000000..fe3df01 --- /dev/null +++ b/pkg/client/handler.go @@ -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) + } +} diff --git a/pkg/game/inventory/event.go b/pkg/game/inventory/event.go new file mode 100644 index 0000000..7fc7d74 --- /dev/null +++ b/pkg/game/inventory/event.go @@ -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" +} diff --git a/pkg/game/inventory/inventory.go b/pkg/game/inventory/inventory.go new file mode 100644 index 0000000..5d2b5a0 --- /dev/null +++ b/pkg/game/inventory/inventory.go @@ -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) +} diff --git a/pkg/game/inventory/manager.go b/pkg/game/inventory/manager.go new file mode 100644 index 0000000..f7b97b6 --- /dev/null +++ b/pkg/game/inventory/manager.go @@ -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) +} diff --git a/pkg/game/player/event.go b/pkg/game/player/event.go new file mode 100644 index 0000000..6d7f303 --- /dev/null +++ b/pkg/game/player/event.go @@ -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" +} diff --git a/pkg/game/player/pathfinding.go b/pkg/game/player/pathfinding.go new file mode 100644 index 0000000..8d3a5f7 --- /dev/null +++ b/pkg/game/player/pathfinding.go @@ -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 +} diff --git a/pkg/game/player/player.go b/pkg/game/player/player.go new file mode 100644 index 0000000..6e04c66 --- /dev/null +++ b/pkg/game/player/player.go @@ -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 +} diff --git a/pkg/game/world/entity.go b/pkg/game/world/entity.go new file mode 100644 index 0000000..bd0d5d9 --- /dev/null +++ b/pkg/game/world/entity.go @@ -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 +} diff --git a/pkg/game/world/event.go b/pkg/game/world/event.go new file mode 100644 index 0000000..4a041ab --- /dev/null +++ b/pkg/game/world/event.go @@ -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" +} diff --git a/pkg/game/world/world.go b/pkg/game/world/world.go new file mode 100644 index 0000000..5a14d19 --- /dev/null +++ b/pkg/game/world/world.go @@ -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 +} diff --git a/pkg/protocol/component/codecs.go b/pkg/protocol/component/codecs.go index 78e66bb..d328ab1 100644 --- a/pkg/protocol/component/codecs.go +++ b/pkg/protocol/component/codecs.go @@ -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 +} diff --git a/pkg/protocol/metadata/metadata.go b/pkg/protocol/metadata/metadata.go index 9656d21..b0841ed 100644 --- a/pkg/protocol/metadata/metadata.go +++ b/pkg/protocol/metadata/metadata.go @@ -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{} } } diff --git a/pkg/protocol/packet/configuration/client/codecs.go b/pkg/protocol/packet/configuration/client/codecs.go index 20b6882..6088480 100644 --- a/pkg/protocol/packet/configuration/client/codecs.go +++ b/pkg/protocol/packet/configuration/client/codecs.go @@ -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 +} diff --git a/pkg/protocol/packet/game/client/codecs.go b/pkg/protocol/packet/game/client/codecs.go index 6c394f6..45881cb 100644 --- a/pkg/protocol/packet/game/client/codecs.go +++ b/pkg/protocol/packet/game/client/codecs.go @@ -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 +} diff --git a/pkg/protocol/packet/game/client/forget_level_chunk.go b/pkg/protocol/packet/game/client/forget_level_chunk.go index cf909cd..5b36478 100644 --- a/pkg/protocol/packet/game/client/forget_level_chunk.go +++ b/pkg/protocol/packet/game/client/forget_level_chunk.go @@ -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 { diff --git a/pkg/protocol/packet/game/client/level_chunk_with_light.go b/pkg/protocol/packet/game/client/level_chunk_with_light.go index 589b755..19bbdd6 100644 --- a/pkg/protocol/packet/game/client/level_chunk_with_light.go +++ b/pkg/protocol/packet/game/client/level_chunk_with_light.go @@ -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 } diff --git a/pkg/protocol/position.go b/pkg/protocol/position.go new file mode 100644 index 0000000..d4526fd --- /dev/null +++ b/pkg/protocol/position.go @@ -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] +} diff --git a/pkg/protocol/slot/item_stack.go b/pkg/protocol/slot/item_stack.go index 10d090d..6bb027e 100644 --- a/pkg/protocol/slot/item_stack.go +++ b/pkg/protocol/slot/item_stack.go @@ -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 diff --git a/readme.md b/readme.md index 853a4f8..e8922db 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,117 @@ -## 說明 +# minego -Minecraft 1.21.6 go-mc server and client \ No newline at end of file +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 \ No newline at end of file