diff --git a/bot/client.go b/bot/client.go index c3e7c4c..9f0f479 100644 --- a/bot/client.go +++ b/bot/client.go @@ -4,6 +4,7 @@ import ( "sync" "time" + "github.com/Tnze/go-mc/bot/path" "github.com/Tnze/go-mc/bot/phy" "github.com/Tnze/go-mc/bot/world" "github.com/Tnze/go-mc/bot/world/entity" @@ -24,7 +25,7 @@ type Client struct { settings Settings Wd world.World //the map data - Inputs phy.Inputs + Inputs path.Inputs Physics phy.State lastPosTx time.Time justTeleported bool diff --git a/bot/path/blocks.go b/bot/path/blocks.go index b6a848f..0f7d42c 100644 --- a/bot/path/blocks.go +++ b/bot/path/blocks.go @@ -7,7 +7,7 @@ import ( var ( safeStepBlocks = make(map[world.BlockStatus]struct{}, 1024) - blocks = []block.Block{ + stepBlocks = []block.Block{ block.Stone, block.Granite, block.PolishedGranite, @@ -34,10 +34,35 @@ var ( block.Sandstone, block.RedstoneOre, } + + safeWalkBlocks = make(map[world.BlockStatus]struct{}, 128) + walkBlocks = []block.Block{ + block.Air, + block.Grass, + block.Torch, + block.OakSign, + block.SpruceSign, + block.BirchSign, + block.AcaciaSign, + block.JungleSign, + block.DarkOakSign, + block.OakWallSign, + block.SpruceWallSign, + block.BirchWallSign, + block.AcaciaWallSign, + block.JungleWallSign, + block.DarkOakWallSign, + block.Cornflower, + } + + additionalCostBlocks = map[*block.Block]int{ + &block.Rail: 120, + &block.PoweredRail: 200, + } ) func init() { - for _, b := range blocks { + for _, b := range stepBlocks { if b.MinStateID == b.MaxStateID { safeStepBlocks[world.BlockStatus(b.MinStateID)] = struct{}{} } else { @@ -46,4 +71,24 @@ func init() { } } } + + for _, b := range walkBlocks { + if b.MinStateID == b.MaxStateID { + safeWalkBlocks[world.BlockStatus(b.MinStateID)] = struct{}{} + } else { + for i := b.MinStateID; i <= b.MaxStateID; i++ { + safeWalkBlocks[world.BlockStatus(i)] = struct{}{} + } + } + } +} + +func SteppableBlock(bID world.BlockStatus) bool { + _, ok := safeStepBlocks[bID] + return ok +} + +func AirLikeBlock(bID world.BlockStatus) bool { + _, ok := safeWalkBlocks[bID] + return ok } diff --git a/bot/phy/inputs.go b/bot/path/inputs.go similarity index 84% rename from bot/phy/inputs.go rename to bot/path/inputs.go index a4b853c..2190ada 100644 --- a/bot/phy/inputs.go +++ b/bot/path/inputs.go @@ -1,8 +1,10 @@ -package phy +package path // Inputs describes the desired movements of the player. type Inputs struct { Yaw, Pitch float64 ThrottleX, ThrottleZ float64 + + Jump bool } diff --git a/bot/path/movement.go b/bot/path/movement.go new file mode 100644 index 0000000..dc37b00 --- /dev/null +++ b/bot/path/movement.go @@ -0,0 +1,179 @@ +package path + +// Movement represents a single type of movement in a path. +type Movement uint8 + +var allMovements = []Movement{TraverseNorth, TraverseSouth, TraverseEast, TraverseWest, + TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast, + DropNorth, DropSouth, DropEast, DropWest, + AscendNorth, AscendSouth, AscendEast, AscendWest, +} + +// Valid movement values. +const ( + Waypoint Movement = iota + TraverseNorth + TraverseSouth + TraverseEast + TraverseWest + TraverseNorthEast + TraverseNorthWest + TraverseSouthEast + TraverseSouthWest + DropNorth + DropSouth + DropEast + DropWest + AscendNorth + AscendSouth + AscendEast + AscendWest +) + +func (m Movement) Possible(nav *Nav, x, y, z int, from V3) bool { + // fmt.Printf("%s.Possible(%d,%d,%d)\n", m, x, y, z) + switch m { + case Waypoint, TraverseNorth, TraverseSouth, TraverseEast, TraverseWest: + if !SteppableBlock(nav.World.GetBlockStatus(x, y, z)) { + return false + } + return AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) && AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) + + case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast: + if !SteppableBlock(nav.World.GetBlockStatus(x, y, z)) { + return false + } + if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) { + return false + } + if !AirLikeBlock(nav.World.GetBlockStatus(from.X, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(from.X, y+2, z)) { + return false + } + return AirLikeBlock(nav.World.GetBlockStatus(x, y+1, from.Z)) && AirLikeBlock(nav.World.GetBlockStatus(x, y+2, from.Z)) + + case DropNorth, DropSouth, DropEast, DropWest: + for amt := 0; amt < 3; amt++ { + if !AirLikeBlock(nav.World.GetBlockStatus(x, y+amt+1, z)) { + return false + } + } + return SteppableBlock(nav.World.GetBlockStatus(x, y, z)) + + case AscendNorth, AscendSouth, AscendEast, AscendWest: + if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) { + return false + } + return SteppableBlock(nav.World.GetBlockStatus(x, y, z)) && + AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+1, from.Z)) && + AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+2, from.Z)) + + default: + panic(m) + } +} + +func (m Movement) Offset() (x, y, z int) { + switch m { + case Waypoint: + return 0, 0, 0 + case TraverseNorth: + return 0, 0, -1 + case TraverseSouth: + return 0, 0, 1 + case TraverseEast: + return 1, 0, 0 + case TraverseWest: + return -1, 0, 0 + + case DropNorth: + return 0, -1, -1 + case DropSouth: + return 0, -1, 1 + case DropEast: + return 1, -1, 0 + case DropWest: + return -1, -1, 0 + case AscendNorth: + return 0, 1, -1 + case AscendSouth: + return 0, 1, 1 + case AscendEast: + return 1, 1, 0 + case AscendWest: + return -1, 1, 0 + + case TraverseNorthWest: + return -1, 0, -1 + case TraverseNorthEast: + return 1, 0, -1 + case TraverseSouthWest: + return -1, 0, 1 + case TraverseSouthEast: + return 1, 0, 1 + + default: + panic(m) + } +} + +func (m Movement) BaseCost() float64 { + switch m { + case Waypoint: + return 0 + case TraverseNorth, TraverseSouth, TraverseEast, TraverseWest: + return 1 + case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast: + return 1.5 + + case DropNorth, DropSouth, DropEast, DropWest: + return 2 + case AscendNorth, AscendSouth, AscendEast, AscendWest: + return 2.25 + default: + panic(m) + } +} + +func (m Movement) String() string { + switch m { + case Waypoint: + return "waypoint" + case TraverseNorth: + return "traverse-north" + case TraverseSouth: + return "traverse-south" + case TraverseEast: + return "traverse-east" + case TraverseWest: + return "traverse-west" + + case DropNorth: + return "drop-north" + case DropSouth: + return "drop-south" + case DropEast: + return "drop-east" + case DropWest: + return "drop-west" + + case AscendNorth: + return "jump-north" + case AscendSouth: + return "jump-south" + case AscendEast: + return "jump-east" + case AscendWest: + return "jump-west" + + case TraverseNorthWest: + return "traverse-northwest" + case TraverseNorthEast: + return "traverse-northeast" + case TraverseSouthWest: + return "traverse-southwest" + case TraverseSouthEast: + return "traverse-southeast" + default: + panic(m) + } +} diff --git a/bot/path/path.go b/bot/path/path.go index d55b9c1..06c39fa 100644 --- a/bot/path/path.go +++ b/bot/path/path.go @@ -2,8 +2,9 @@ package path import ( + "math" + "github.com/Tnze/go-mc/bot/world" - "github.com/Tnze/go-mc/data/block" "github.com/beefsack/go-astar" ) @@ -36,27 +37,14 @@ func (n *Nav) Path() (path []astar.Pather, distance float64, found bool) { }) } -// Movement represents a single type of movement in a path. -type Movement uint8 - -var allMovements = []Movement{TraverseNorth, TraverseSouth, TraverseEast, TraverseWest} - -// Valid movement values. -const ( - Waypoint Movement = iota - TraverseNorth - TraverseSouth - TraverseEast - TraverseWest -) - // Tile represents a point in a path. All tiles in a path are adjaceent their // preceeding tiles. type Tile struct { Nav *Nav - Movement Movement - Pos V3 + Movement Movement + Pos V3 + ExtraCost int } func (t Tile) PathNeighborCost(to astar.Pather) float64 { @@ -67,12 +55,17 @@ func (t Tile) PathNeighborCost(to astar.Pather) float64 { func (t Tile) PathEstimatedCost(to astar.Pather) float64 { other := to.(Tile) cost := t.Pos.Cost(other.Pos) + return cost + other.Movement.BaseCost() } func (t Tile) PathNeighbors() []astar.Pather { possibles := make([]astar.Pather, 0, 8) + if t.PathEstimatedCost(Tile{Pos: t.Nav.Start}) > 1200 { + return nil + } + if t.Pos == t.Nav.Dest && t.Movement != Waypoint { dupe := t dupe.Movement = Waypoint @@ -82,7 +75,7 @@ func (t Tile) PathNeighbors() []astar.Pather { for _, m := range allMovements { x, y, z := m.Offset() pos := V3{X: t.Pos.X + x, Y: t.Pos.Y + y, Z: t.Pos.Z + z} - if m.Possible(t.Nav, pos.X, pos.Y, pos.Z) { + if m.Possible(t.Nav, pos.X, pos.Y, pos.Z, t.Pos) { possibles = append(possibles, Tile{ Nav: t.Nav, Movement: m, @@ -95,63 +88,18 @@ func (t Tile) PathNeighbors() []astar.Pather { return possibles } -func (m Movement) Possible(nav *Nav, x, y, z int) bool { - // fmt.Printf("%s.Possible(%d,%d,%d)\n", m, x, y, z) - switch m { - case Waypoint, TraverseNorth, TraverseSouth, TraverseEast, TraverseWest: - b := nav.World.GetBlockStatus(x, y, z) - if _, safe := safeStepBlocks[b]; !safe { - return false - } - above1 := uint32(nav.World.GetBlockStatus(x, y+1, z)) - above2 := uint32(nav.World.GetBlockStatus(x, y+2, z)) - return above1 == block.Air.MinStateID && above2 == block.Air.MinStateID - default: - panic(m) +func (t Tile) Inputs(dX, dY, dZ float64) Inputs { + // Sufficient for simple movements. + at := math.Atan2(-dX, -dZ) + out := Inputs{ + ThrottleX: math.Sin(at), + ThrottleZ: math.Cos(at), } -} -func (m Movement) Offset() (x, y, z int) { - switch m { - case Waypoint: - return 0, 0, 0 - case TraverseNorth: - return 0, 0, -1 - case TraverseSouth: - return 0, 0, 1 - case TraverseEast: - return 1, 0, 0 - case TraverseWest: - return -1, 0, 0 - default: - panic(m) - } -} - -func (m Movement) BaseCost() float64 { - switch m { - case Waypoint: - return 0 - case TraverseNorth, TraverseSouth, TraverseEast, TraverseWest: - return 1 - default: - panic(m) - } -} - -func (m Movement) String() string { - switch m { - case Waypoint: - return "waypoint" - case TraverseNorth: - return "traverse-north" - case TraverseSouth: - return "traverse-south" - case TraverseEast: - return "traverse-east" - case TraverseWest: - return "traverse-west" - default: - panic(m) + switch t.Movement { + case AscendNorth, AscendSouth, AscendEast, AscendWest: + out.Jump = math.Sqrt(dX*dX+dZ*dZ) < 1.75 + out.Yaw = 0 } + return out } diff --git a/bot/phy/phy.go b/bot/phy/phy.go index 3d97490..6953f14 100644 --- a/bot/phy/phy.go +++ b/bot/phy/phy.go @@ -5,6 +5,7 @@ package phy import ( "math" + "github.com/Tnze/go-mc/bot/path" "github.com/Tnze/go-mc/bot/world" "github.com/Tnze/go-mc/bot/world/entity/player" ) @@ -17,6 +18,9 @@ const ( maxYawChange = 33 maxPitchChange = 11 + stepHeight = 0.6 + minJumpTicks = 14 + gravity = 0.08 drag = 0.98 acceleration = 0.02 @@ -43,6 +47,7 @@ type State struct { Pos Point Vel Point Yaw, Pitch float64 + lastJump uint32 // player state flags. onGround bool @@ -51,7 +56,8 @@ type State struct { horizontal bool } - Run bool + tick uint32 + Run bool } func (s *State) ServerPositionUpdate(player player.Pos, w World) error { @@ -79,7 +85,7 @@ func (s *State) surroundings(query AABB, w World) Surrounds { 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 { + if block := w.GetBlockStatus(x, y, z); !path.AirLikeBlock(block) { 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))) } } @@ -88,89 +94,6 @@ func (s *State) surroundings(query AABB, w World) Surrounds { return out } -func (s *State) applyLookInputs(input Inputs) { - errYaw := math.Min(math.Max(input.Yaw-s.Yaw, -maxYawChange), maxYawChange) - s.Yaw += errYaw - errPitch := math.Min(math.Max(input.Pitch-s.Pitch, -maxPitchChange), maxPitchChange) - s.Pitch += errPitch -} - -func (s *State) applyPosInputs(input Inputs, acceleration, inertia float64) { - speed := math.Sqrt(input.ThrottleX*input.ThrottleX + input.ThrottleZ*input.ThrottleZ) - if speed < 0.01 { - return - } - speed = acceleration / math.Max(speed, 1) - - input.ThrottleX *= speed - input.ThrottleZ *= speed - - s.Vel.X += input.ThrottleX - s.Vel.Z += input.ThrottleZ -} - -func (s *State) Tick(input Inputs, w World) error { - if !s.Run { - return nil - } - var inertia = inertia - var acceleration = acceleration - if s.onGround { - inertia *= slipperiness - acceleration = 0.1 * (0.1627714 / (inertia * inertia * inertia)) - } - s.applyLookInputs(input) - s.applyPosInputs(input, acceleration, inertia) - - // Deadzone velocities when they get too low. - 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 - } - // Gravity - s.Vel.Y -= gravity - // Drag & friction. - s.Vel.Y *= drag - s.Vel.X *= inertia - s.Vel.Z *= inertia - - // 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}, @@ -186,3 +109,114 @@ func (s *State) Position() player.Pos { OnGround: s.onGround, } } + +func (s *State) Tick(input path.Inputs, w World) error { + s.tick++ + if !s.Run { + return nil + } + s.tickVelocity(input, w) + + player, newVel := s.computeCollision(s.BB(), s.BB().Extend(s.Vel.X, s.Vel.Y, s.Vel.Z), w) + + bb := player.Extend(s.Vel.X, stepHeight, s.Vel.Z) + surroundings := s.surroundings(bb, w) + y := float64(0) + for _, b := range surroundings { + if b.Intersects(bb) && bb.Y.Max > b.Y.Min { + y = math.Max(y, b.Y.Max) + } + } + //fmt.Printf("pY = %.2f, maxblockY = %.1f (delta = %.1f)\n", bb.Y.Min, y, bb.Y.Min-y) + if d := bb.Y.Min - y; d >= -stepHeight && d < stepHeight-1 { + bb := player.Offset(0, stepHeight, 0) + player, newVel = s.computeCollision(bb, bb.Extend(s.Vel.X, s.Vel.Y, s.Vel.Z), w) + } + + // 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) applyLookInputs(input path.Inputs) { + errYaw := math.Min(math.Max(input.Yaw-s.Yaw, -maxYawChange), maxYawChange) + s.Yaw += errYaw + errPitch := math.Min(math.Max(input.Pitch-s.Pitch, -maxPitchChange), maxPitchChange) + s.Pitch += errPitch +} + +func (s *State) applyPosInputs(input path.Inputs, acceleration, inertia float64) { + // fmt.Println(input.Jump, s.lastJump, s.onGround) + if input.Jump && s.lastJump+minJumpTicks < s.tick { + s.lastJump = s.tick + s.Vel.Y += 0.42 + } + + speed := math.Sqrt(input.ThrottleX*input.ThrottleX + input.ThrottleZ*input.ThrottleZ) + if speed < 0.01 { + return + } + speed = acceleration / math.Max(speed, 1) + + input.ThrottleX *= speed + input.ThrottleZ *= speed + + s.Vel.X += input.ThrottleX + s.Vel.Z += input.ThrottleZ +} + +func (s *State) tickVelocity(input path.Inputs, w World) { + var inertia = inertia + var acceleration = acceleration + if below := w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y))-1, int(math.Floor(s.Pos.Z))); s.onGround && !path.AirLikeBlock(below) { + inertia *= slipperiness + acceleration = 0.1 * (0.1627714 / (inertia * inertia * inertia)) + } + + // Deadzone velocities when they get too low. + 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.applyLookInputs(input) + s.applyPosInputs(input, acceleration, inertia) + + // Gravity + s.Vel.Y -= gravity + // Drag & friction. + s.Vel.Y *= drag + s.Vel.X *= inertia + s.Vel.Z *= inertia +} + +func (s *State) computeCollision(bb, query AABB, w World) (outBB AABB, outVel Point) { + surroundings := s.surroundings(query, w) + outVel = s.Vel + + for _, b := range surroundings { + outVel.Y = b.YOffset(bb, outVel.Y) + } + bb = bb.Offset(0, outVel.Y, 0) + for _, b := range surroundings { + outVel.X = b.XOffset(bb, outVel.X) + } + bb = bb.Offset(outVel.X, 0, 0) + for _, b := range surroundings { + outVel.Z = b.ZOffset(bb, outVel.Z) + } + bb = bb.Offset(0, 0, outVel.Z) + return bb, outVel +}