From 70bb24a7fbb7730e45124b95444b480550fb31c9 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 16 Sep 2020 20:16:47 -0700 Subject: [PATCH] Major update to implement basic collision --- bot/client.go | 43 +++++--- bot/event.go | 4 + bot/ingame.go | 159 +++++++++++++++++++++++------- bot/mcbot.go | 2 +- bot/motion.go | 2 +- bot/phy/aabb.go | 128 ++++++++++++++++++++++++ bot/phy/phy.go | 145 +++++++++++++++++++++++++++ bot/world/entity/player/player.go | 20 +++- net/ptypes/motion.go | 52 ++++++++++ 9 files changed, 501 insertions(+), 54 deletions(-) create mode 100644 bot/phy/aabb.go create mode 100644 bot/phy/phy.go diff --git a/bot/client.go b/bot/client.go index a5eee4d..226df7a 100644 --- a/bot/client.go +++ b/bot/client.go @@ -1,10 +1,15 @@ package bot import ( + "sync" + "time" + + "github.com/Tnze/go-mc/bot/phy" "github.com/Tnze/go-mc/bot/world" "github.com/Tnze/go-mc/bot/world/entity" "github.com/Tnze/go-mc/bot/world/entity/player" "github.com/Tnze/go-mc/net" + pk "github.com/Tnze/go-mc/net/packet" ) // Client is used to access Minecraft server @@ -17,12 +22,26 @@ type Client struct { ServInfo abilities PlayerAbilities settings Settings + Wd world.World //the map data + Physics phy.State + lastPosTx time.Time // Delegate allows you push a function to let HandleGame run. // Do not send at the same goroutine! Delegate chan func() error Events eventBroker + + closing chan struct{} + inbound chan pk.Packet + wg sync.WaitGroup +} + +func (c *Client) Close() error { + close(c.closing) + err := c.disconnect() + c.wg.Wait() + return err } // NewClient init and return a new Client. @@ -32,20 +51,18 @@ type Client struct { // // For online-mode, you need login your Mojang account // and load your Name, UUID and AccessToken to client. -func NewClient() (c *Client) { - c = new(Client) - - //init Client - c.settings = DefaultSettings - c.Name = "Steve" - c.Delegate = make(chan func() error) - - c.Wd = world.World{ - Entities: make(map[int32]entity.Entity), - Chunks: make(map[world.ChunkLoc]*world.Chunk), +func NewClient() *Client { + return &Client{ + settings: DefaultSettings, + Auth: Auth{Name: "Steve"}, + Delegate: make(chan func() error), + Wd: world.World{ + Entities: make(map[int32]entity.Entity), + Chunks: make(map[world.ChunkLoc]*world.Chunk), + }, + closing: make(chan struct{}), + inbound: make(chan pk.Packet, 5), } - - return } //PlayInfo content player info in server. diff --git a/bot/event.go b/bot/event.go index 10ec415..44ec23e 100644 --- a/bot/event.go +++ b/bot/event.go @@ -2,6 +2,7 @@ package bot import ( "github.com/Tnze/go-mc/bot/world/entity" + "github.com/Tnze/go-mc/bot/world/entity/player" "github.com/Tnze/go-mc/chat" "github.com/google/uuid" @@ -64,6 +65,9 @@ type eventBroker struct { // things. GameReady func() error + // PositionChange is called whenever the player position is updated. + PositionChange func(pos player.Pos) error + // ReceivePacket will be called when new packets arrive. // The default handler will run only if pass == false. ReceivePacket func(p pk.Packet) (pass bool, err error) diff --git a/bot/ingame.go b/bot/ingame.go index 79d2e14..6bd24fd 100644 --- a/bot/ingame.go +++ b/bot/ingame.go @@ -3,44 +3,101 @@ package bot import ( "bytes" "fmt" + "net" + "net/http" + "os" + "time" "github.com/google/uuid" "github.com/Tnze/go-mc/bot/world" + "github.com/Tnze/go-mc/bot/world/entity/player" "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data" pk "github.com/Tnze/go-mc/net/packet" "github.com/Tnze/go-mc/net/ptypes" ) -// //GetPosition return the player's position -// func (p *Player) GetPosition() (x, y, z float64) { -// return p.X, p.Y, p.Z -// } +func (c *Client) updateServerPos(pos player.Pos) error { + prev := c.Player.Pos + c.Player.Pos = pos -// //GetBlockPos return the position of the Block at player's feet -// func (p *Player) GetBlockPos() (x, y, z int) { -// return int(math.Floor(p.X)), int(math.Floor(p.Y)), int(math.Floor(p.Z)) -// } + switch { + case !prev.LookEqual(pos) && !prev.PosEqual(pos): + sendPlayerPositionAndLookPacket(c) + case !prev.PosEqual(pos): + sendPlayerPositionPacket(c) + case !prev.LookEqual(pos): + sendPlayerLookPacket(c) + case prev.OnGround != pos.OnGround: + c.conn.WritePacket( + //ClientSettings packet (serverbound) + pk.Marshal( + data.Flying, + pk.Boolean(pos.OnGround), + ), + ) + case time.Now().Add(-time.Second).After(c.lastPosTx): + c.lastPosTx = time.Now() + sendPlayerPositionPacket(c) + } + + if c.Events.PositionChange != nil && !prev.Equal(pos) { + if err := c.Events.PositionChange(pos); err != nil { + return err + } + } + return nil +} // HandleGame receive server packet and response them correctly. // Note that HandleGame will block if you don't receive from Events. func (c *Client) HandleGame() error { + c.wg.Add(1) + go func() { + defer c.wg.Done() + for { + select { + case <-c.closing: + return + + default: + //Read packets + p, err := c.conn.ReadPacket() + if err != nil { + if e, ok := err.(*net.OpError); ok && e.Err.Error() != "use of closed network connection" { + fmt.Fprintf(os.Stderr, "ReadPacket error: %v\n", err) + } + return + } + c.inbound <- p + } + } + }() + + cTick := time.NewTicker(time.Second / 10 / 2) + defer cTick.Stop() for { select { - case task := <-c.Delegate: - if err := task(); err != nil { + case <-c.closing: + return http.ErrServerClosed + case <-cTick.C: + if err := c.Physics.Tick(&c.Wd); err != nil { + c.disconnect() return err } - default: - //Read packets - p, err := c.conn.ReadPacket() - if err != nil { - return fmt.Errorf("bot: read packet fail: %w", err) + c.updateServerPos(c.Physics.Position()) + + case task := <-c.Delegate: + if err := task(); err != nil { + c.disconnect() + return err } + case p := <-c.inbound: //handle packets disconnect, err := c.handlePacket(p) if err != nil { + c.disconnect() return fmt.Errorf("handle packet 0x%X error: %w", p.ID, err) } if disconnect { @@ -270,9 +327,9 @@ func handleUpdateHealthPacket(c *Client, p pk.Packet) error { } } if c.Health < 1 { //player is dead + c.Physics.Run = false sendPlayerPositionAndLookPacket(c) if c.Events.Die != nil { - if err := c.Events.Die(); err != nil { return err } @@ -434,30 +491,41 @@ func handlePlayerPositionAndLookPacket(c *Client, p pk.Packet) error { return err } + pp := c.Player.Pos if pkt.RelativeX() { - c.X = float64(pkt.X) + pp.X += float64(pkt.X) } else { - c.X += float64(pkt.X) + pp.X = float64(pkt.X) } if pkt.RelativeY() { - c.Y = float64(pkt.Y) + pp.Y += float64(pkt.Y) } else { - c.Y += float64(pkt.Y) + pp.Y = float64(pkt.Y) } if pkt.RelativeZ() { - c.Z = float64(pkt.Z) + pp.Z += float64(pkt.Z) } else { - c.Z += float64(pkt.Z) + pp.Z = float64(pkt.Z) } if pkt.RelativeYaw() { - c.Yaw = float32(pkt.Yaw) + pp.Yaw += float32(pkt.Yaw) } else { - c.Yaw += float32(pkt.Yaw) + pp.Yaw = float32(pkt.Yaw) } if pkt.RelativePitch() { - c.Pitch = float32(pkt.Pitch) + pp.Pitch += float32(pkt.Pitch) } else { - c.Pitch += float32(pkt.Pitch) + pp.Pitch = float32(pkt.Pitch) + } + if err := c.Physics.ServerPositionUpdate(pp, &c.Wd); err != nil { + return err + } + c.Player.Pos = pp + + if c.Events.PositionChange != nil { + if err := c.Events.PositionChange(pp); err != nil { + return err + } } //Confirm @@ -516,14 +584,33 @@ func handleSetExperience(c *Client, p pk.Packet) (err error) { return nil } -func sendPlayerPositionAndLookPacket(c *Client) { - c.conn.WritePacket(pk.Marshal( - data.PositionLook, - pk.Double(c.X), - pk.Double(c.Y), - pk.Double(c.Z), - pk.Float(c.Yaw), - pk.Float(c.Pitch), - pk.Boolean(c.OnGround), - )) +func sendPlayerPositionAndLookPacket(c *Client) error { + // fmt.Println("PPL") + return c.conn.WritePacket(ptypes.PositionAndLookServerbound{ + X: pk.Double(c.Pos.X), + Y: pk.Double(c.Pos.Y), + Z: pk.Double(c.Pos.Z), + Yaw: pk.Float(c.Pos.Yaw), + Pitch: pk.Float(c.Pos.Pitch), + OnGround: pk.Boolean(c.Pos.OnGround), + }.Encode()) +} + +func sendPlayerPositionPacket(c *Client) error { + // fmt.Println("P") + return c.conn.WritePacket(ptypes.Position{ + X: pk.Double(c.Pos.X), + Y: pk.Double(c.Pos.Y), + Z: pk.Double(c.Pos.Z), + OnGround: pk.Boolean(c.Pos.OnGround), + }.Encode()) +} + +func sendPlayerLookPacket(c *Client) error { + // fmt.Println("L") + return c.conn.WritePacket(ptypes.Look{ + Yaw: pk.Float(c.Pos.Yaw), + Pitch: pk.Float(c.Pos.Pitch), + OnGround: pk.Boolean(c.Pos.OnGround), + }.Encode()) } diff --git a/bot/mcbot.go b/bot/mcbot.go index 1b5310a..4b5b3b7 100644 --- a/bot/mcbot.go +++ b/bot/mcbot.go @@ -98,7 +98,7 @@ func (c *Client) join(d Dialer, addr string) (err error) { case 0x02: //Login Success // uuid, l := pk.UnpackString(pack.Data) // name, _ := unpackString(pack.Data[l:]) - return //switches the connection state to PLAY. + return nil case 0x03: //Set Compression var threshold pk.VarInt if err := pack.Scan(&threshold); err != nil { diff --git a/bot/motion.go b/bot/motion.go index 4a19378..277f3c1 100644 --- a/bot/motion.go +++ b/bot/motion.go @@ -180,7 +180,7 @@ func (c *Client) SwapItem() error { // Disconnect disconnect the server. // Server will close the connection. -func (c *Client) Disconnect() error { +func (c *Client) disconnect() error { return c.conn.Close() } diff --git a/bot/phy/aabb.go b/bot/phy/aabb.go new file mode 100644 index 0000000..e20ac4e --- /dev/null +++ b/bot/phy/aabb.go @@ -0,0 +1,128 @@ +package phy + +import ( + "math" + + "github.com/Tnze/go-mc/bot/world" +) + +type MinMax struct { + Min, Max float64 +} + +func (mm MinMax) Extend(delta float64) MinMax { + if delta < 0 { + return MinMax{ + Min: mm.Min + delta, + Max: mm.Max, + } + } + + return MinMax{ + Min: mm.Min, + Max: mm.Max + delta, + } +} + +func (mm MinMax) Contract(amt float64) MinMax { + return MinMax{ + Min: mm.Min + amt, + Max: mm.Max - amt, + } +} + +func (mm MinMax) Expand(amt float64) MinMax { + return MinMax{ + Min: mm.Min - amt, + Max: mm.Max + amt, + } +} + +func (mm MinMax) Offset(amt float64) MinMax { + return MinMax{ + Min: mm.Min + amt, + Max: mm.Max + amt, + } +} + +// AABB implements Axis Aligned Bounding Box operations. +type AABB struct { + X, Y, Z MinMax + Block world.BlockStatus +} + +func (bb AABB) Extend(dx, dy, dz float64) AABB { + return AABB{ + X: bb.X.Extend(dx), + Y: bb.Y.Extend(dx), + Z: bb.Z.Extend(dx), + Block: bb.Block, + } +} + +func (bb AABB) Contract(x, y, z float64) AABB { + return AABB{ + X: bb.X.Contract(x), + Y: bb.Y.Contract(y), + Z: bb.Z.Contract(z), + Block: bb.Block, + } +} + +func (bb AABB) Expand(x, y, z float64) AABB { + return AABB{ + X: bb.X.Expand(x), + Y: bb.Y.Expand(y), + Z: bb.Z.Expand(z), + Block: bb.Block, + } +} + +func (bb AABB) Offset(x, y, z float64) AABB { + return AABB{ + X: bb.X.Offset(x), + Y: bb.Y.Offset(y), + Z: bb.Z.Offset(z), + Block: bb.Block, + } +} + +func (bb AABB) XOffset(o AABB, xOffset float64) float64 { + if o.Y.Max > bb.Y.Min && o.Y.Min < bb.Y.Max && o.Z.Max > bb.Z.Min && o.Z.Min < bb.Z.Max { + if xOffset > 0.0 && o.X.Max <= bb.X.Min { + xOffset = math.Min(bb.X.Min-o.X.Max, xOffset) + } else if xOffset < 0.0 && o.X.Min >= bb.X.Max { + xOffset = math.Max(bb.X.Max-o.X.Min, xOffset) + } + } + return xOffset +} + +func (bb AABB) YOffset(o AABB, yOffset float64) float64 { + if o.X.Max > bb.X.Min && o.X.Min < bb.X.Max && o.Z.Max > bb.Z.Min && o.Z.Min < bb.Z.Max { + if yOffset > 0.0 && o.Y.Max <= bb.Y.Min { + yOffset = math.Min(bb.Y.Min-o.Y.Max, yOffset) + } else if yOffset < 0.0 && o.Y.Min >= bb.Y.Max { + yOffset = math.Max(bb.Y.Max-o.Y.Min, yOffset) + } + } + return yOffset +} + +func (bb AABB) ZOffset(o AABB, zOffset float64) float64 { + if o.X.Max > bb.X.Min && o.X.Min < bb.X.Max && o.Y.Max > bb.Y.Min && o.Y.Min < bb.Y.Max { + if zOffset > 0.0 && o.Z.Max <= bb.Z.Min { + zOffset = math.Min(bb.Z.Min-o.Z.Max, zOffset) + } else if zOffset < 0.0 && o.Z.Min >= bb.Z.Max { + zOffset = math.Max(bb.Z.Max-o.Z.Min, zOffset) + } + } + return zOffset +} + +func (bb AABB) Intersects(o AABB) bool { + return true && + bb.X.Min < o.X.Max && bb.X.Max > o.X.Min && + bb.Y.Min < o.Y.Max && bb.Y.Max > o.Y.Min && + bb.Z.Min < o.Z.Max && bb.Z.Max > o.Z.Min +} diff --git a/bot/phy/phy.go b/bot/phy/phy.go new file mode 100644 index 0000000..fa14360 --- /dev/null +++ b/bot/phy/phy.go @@ -0,0 +1,145 @@ +// Package phy implements a minimal physics simulation necessary for realistic +// bot behavior. +package phy + +import ( + "math" + + "github.com/Tnze/go-mc/bot/world" + "github.com/Tnze/go-mc/bot/world/entity/player" +) + +const ( + playerWidth = 0.6 + playerHeight = 1.8 + resetVel = 0.003 + + gravity = 0.08 +) + +// World represents a provider of information about the surrounding world. +type World interface { + GetBlockStatus(x, y, z int) world.BlockStatus +} + +// Surrounds represents the blocks surrounding the player (Y, Z, X). +type Surrounds []AABB + +// Point represents a point in 3D space. +type Point struct { + X, Y, Z float64 +} + +// State tracks physics state. +type State struct { + // player state. + Pos Point + Vel Point + Yaw, Pitch float64 + + // player state flags. + onGround bool + collision struct { + vertical bool + horizontal bool + } + + Run bool +} + +func (s *State) ServerPositionUpdate(player player.Pos, w World) error { + s.Pos = Point{X: player.X, Y: player.Y, Z: player.Z} + s.Yaw, s.Pitch = float64(player.Yaw), float64(player.Pitch) + s.Vel = Point{} + s.onGround, s.collision.vertical, s.collision.horizontal = false, false, false + s.Run = true + return nil +} + +func abs(i1, i2 int) int { + if i1 < i2 { + return i2 - i1 + } + return i1 - i2 +} + +func (s *State) surroundings(query AABB, w World) Surrounds { + minY, maxY := int(math.Floor(query.Y.Min))-1, int(math.Floor(query.Y.Max))+1 + minZ, maxZ := int(math.Floor(query.Z.Min)), int(math.Floor(query.Z.Max))+1 + minX, maxX := int(math.Floor(query.X.Min)), int(math.Floor(query.X.Max))+1 + + out := Surrounds(make([]AABB, 0, abs(maxY, minY)*abs(maxZ, minZ)*abs(maxX, minX))) + for y := minY; y < maxY; y++ { + for z := minZ; z < maxZ; z++ { + for x := minX; x < maxX; x++ { + if block := w.GetBlockStatus(x, y, z); block > 0 { + out = append(out, AABB{X: MinMax{Max: 1}, Y: MinMax{Max: 1}, Z: MinMax{Max: 1}, Block: block}.Offset(float64(x), float64(y), float64(z))) + } + } + } + } + return out +} + +func (s *State) Tick(w World) error { + if !s.Run { + return nil + } + if math.Abs(s.Vel.X) < resetVel { + s.Vel.X = 0 + } + if math.Abs(s.Vel.Y) < resetVel { + s.Vel.Y = 0 + } + if math.Abs(s.Vel.Z) < resetVel { + s.Vel.Z = 0 + } + s.Vel.Y -= gravity + + // Apply collision. + var ( + player = s.BB() + query = player.Extend(s.Vel.X, s.Vel.Y, s.Vel.Z) + surroundings = s.surroundings(query, w) + newVel = s.Vel + ) + for _, b := range surroundings { + newVel.Y = b.YOffset(player, newVel.Y) + } + player = player.Offset(0, newVel.Y, 0) + for _, b := range surroundings { + newVel.X = b.XOffset(player, newVel.X) + } + player = player.Offset(newVel.X, 0, 0) + for _, b := range surroundings { + newVel.Z = b.ZOffset(player, newVel.Z) + } + player = player.Offset(0, 0, newVel.Z) + + // Update flags. + s.Pos.X = player.X.Min + playerWidth/2 + s.Pos.Y = player.Y.Min + s.Pos.Z = player.Z.Min + playerWidth/2 + s.collision.horizontal = newVel.X != s.Vel.X || newVel.Z != s.Vel.Z + s.collision.vertical = newVel.Y != s.Vel.Y + s.onGround = s.collision.vertical && s.Vel.Y < 0 + + s.Vel = newVel + return nil +} + +func (s *State) BB() AABB { + return AABB{ + X: MinMax{Min: -playerWidth / 2, Max: playerWidth / 2}, + Y: MinMax{Max: playerHeight}, + Z: MinMax{Min: -playerWidth / 2, Max: playerWidth / 2}, + }.Offset(s.Pos.X, s.Pos.Y, s.Pos.Z) +} + +func (s *State) Position() player.Pos { + return player.Pos{ + X: s.Pos.X, Y: s.Pos.Y, Z: s.Pos.Z, + Yaw: float32(s.Yaw), Pitch: float32(s.Pitch), + OnGround: s.onGround, + } +} diff --git a/bot/world/entity/player/player.go b/bot/world/entity/player/player.go index 9a30332..6f7bcdb 100644 --- a/bot/world/entity/player/player.go +++ b/bot/world/entity/player/player.go @@ -2,14 +2,28 @@ package player import "github.com/Tnze/go-mc/bot/world/entity" +type Pos struct { + X, Y, Z float64 + Yaw, Pitch float32 + OnGround bool +} + +func (p Pos) PosEqual(other Pos) bool { + return p.X == other.X && p.Y == other.Y && p.Z == other.Z +} +func (p Pos) LookEqual(other Pos) bool { + return p.Yaw == other.Yaw && p.Pitch == other.Pitch +} +func (p Pos) Equal(other Pos) bool { + return p.PosEqual(other) && p.LookEqual(other) && p.OnGround == other.OnGround +} + // Player includes the player's status. type Player struct { entity.Entity UUID [2]int64 //128bit UUID - X, Y, Z float64 - Yaw, Pitch float32 - OnGround bool + Pos Pos HeldItem int //拿着的物品栏位 diff --git a/net/ptypes/motion.go b/net/ptypes/motion.go index 43b1ab0..eacfc4e 100644 --- a/net/ptypes/motion.go +++ b/net/ptypes/motion.go @@ -2,6 +2,7 @@ package ptypes import ( + "github.com/Tnze/go-mc/data" pk "github.com/Tnze/go-mc/net/packet" ) @@ -33,3 +34,54 @@ func (p *PositionAndLookClientbound) RelativePitch() bool { func (p *PositionAndLookClientbound) Decode(pkt pk.Packet) error { return pkt.Scan(&p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, &p.Flags, &p.TeleportID) } + +// PositionAndLookServerbound describes the location and orientation of +// the player. +type PositionAndLookServerbound struct { + X, Y, Z pk.Double + Yaw, Pitch pk.Float + OnGround pk.Boolean +} + +func (p PositionAndLookServerbound) Encode() pk.Packet { + return pk.Marshal( + data.PositionLook, + pk.Double(p.X), + pk.Double(p.Y), + pk.Double(p.Z), + pk.Float(p.Yaw), + pk.Float(p.Pitch), + pk.Boolean(p.OnGround), + ) +} + +// Position describes the position of the player. +type Position struct { + X, Y, Z pk.Double + OnGround pk.Boolean +} + +func (p Position) Encode() pk.Packet { + return pk.Marshal( + data.PositionServerbound, + pk.Double(p.X), + pk.Double(p.Y), + pk.Double(p.Z), + pk.Boolean(p.OnGround), + ) +} + +// Look describes the rotation of the player. +type Look struct { + Yaw, Pitch pk.Float + OnGround pk.Boolean +} + +func (p Look) Encode() pk.Packet { + return pk.Marshal( + data.PositionLook, + pk.Float(p.Yaw), + pk.Float(p.Pitch), + pk.Boolean(p.OnGround), + ) +}