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

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)
}
}