add player package with key client-side player functionality, update protocol codecs, and refactor metadata definitions and slot usage
This commit is contained in:
323
pkg/game/player/player.go
Normal file
323
pkg/game/player/player.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user