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

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

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

View File

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

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

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