add player package with key client-side player functionality, update protocol codecs, and refactor metadata definitions and slot usage
This commit is contained in:
13
pkg/game/inventory/event.go
Normal file
13
pkg/game/inventory/event.go
Normal file
@ -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"
|
||||
}
|
96
pkg/game/inventory/inventory.go
Normal file
96
pkg/game/inventory/inventory.go
Normal file
@ -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)
|
||||
}
|
98
pkg/game/inventory/manager.go
Normal file
98
pkg/game/inventory/manager.go
Normal file
@ -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)
|
||||
}
|
11
pkg/game/player/event.go
Normal file
11
pkg/game/player/event.go
Normal 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"
|
||||
}
|
188
pkg/game/player/pathfinding.go
Normal file
188
pkg/game/player/pathfinding.go
Normal 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
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
|
||||
}
|
59
pkg/game/world/entity.go
Normal file
59
pkg/game/world/entity.go
Normal file
@ -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
|
||||
}
|
19
pkg/game/world/event.go
Normal file
19
pkg/game/world/event.go
Normal file
@ -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"
|
||||
}
|
304
pkg/game/world/world.go
Normal file
304
pkg/game/world/world.go
Normal file
@ -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
|
||||
}
|
Reference in New Issue
Block a user