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

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

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