Merge pull request #84 from twitchyliquid64/master

Massive update
This commit is contained in:
Tnze
2020-10-10 18:30:31 -05:00
committed by GitHub
50 changed files with 30586 additions and 112916 deletions

View File

@ -1,6 +1,6 @@
# Go-MC # Go-MC
![Version](https://img.shields.io/badge/Minecraft-1.16.2-blue.svg) ![Version](https://img.shields.io/badge/Minecraft-1.16.3-blue.svg)
![Protocol](https://img.shields.io/badge/Protocol-751-blue.svg) ![Protocol](https://img.shields.io/badge/Protocol-753-blue.svg)
[![GoDoc](https://godoc.org/github.com/Tnze/go-mc?status.svg)](https://godoc.org/github.com/Tnze/go-mc) [![GoDoc](https://godoc.org/github.com/Tnze/go-mc?status.svg)](https://godoc.org/github.com/Tnze/go-mc)
[![Go Report Card](https://goreportcard.com/badge/github.com/Tnze/go-mc)](https://goreportcard.com/report/github.com/Tnze/go-mc) [![Go Report Card](https://goreportcard.com/badge/github.com/Tnze/go-mc)](https://goreportcard.com/report/github.com/Tnze/go-mc)
[![Build Status](https://travis-ci.org/Tnze/go-mc.svg?branch=master)](https://travis-ci.org/Tnze/go-mc) [![Build Status](https://travis-ci.org/Tnze/go-mc.svg?branch=master)](https://travis-ci.org/Tnze/go-mc)

View File

@ -1,10 +1,18 @@
package bot package bot
import ( 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"
"github.com/Tnze/go-mc/bot/world/entity" "github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/bot/world/entity/player" "github.com/Tnze/go-mc/bot/world/entity/player"
"github.com/Tnze/go-mc/data"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/net/packet"
pk "github.com/Tnze/go-mc/net/packet"
) )
// Client is used to access Minecraft server // Client is used to access Minecraft server
@ -14,14 +22,35 @@ type Client struct {
player.Player player.Player
PlayInfo PlayInfo
ServInfo
abilities PlayerAbilities abilities PlayerAbilities
settings Settings settings Settings
Wd world.World //the map data
Wd world.World //the map data
Inputs path.Inputs
Physics phy.State
lastPosTx time.Time
justTeleported bool
// Delegate allows you push a function to let HandleGame run. // Delegate allows you push a function to let HandleGame run.
// Do not send at the same goroutine! // Do not send at the same goroutine!
Delegate chan func() error Delegate chan func() error
Events eventBroker 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
}
func (c *Client) SendCloseWindow(windowID byte) error {
return c.conn.WritePacket(packet.Marshal(data.CloseWindowServerbound, pk.UnsignedByte(windowID)))
} }
// NewClient init and return a new Client. // NewClient init and return a new Client.
@ -31,34 +60,37 @@ type Client struct {
// //
// For online-mode, you need login your Mojang account // For online-mode, you need login your Mojang account
// and load your Name, UUID and AccessToken to client. // and load your Name, UUID and AccessToken to client.
func NewClient() (c *Client) { func NewClient() *Client {
c = new(Client) return &Client{
settings: DefaultSettings,
//init Client Auth: Auth{Name: "Steve"},
c.settings = DefaultSettings Delegate: make(chan func() error),
c.Name = "Steve" Wd: world.World{
c.Delegate = make(chan func() error) Entities: make(map[int32]*entity.Entity, 8192),
Chunks: make(map[world.ChunkLoc]*world.Chunk, 2048),
c.Wd = world.World{ },
Entities: make(map[int32]entity.Entity), closing: make(chan struct{}),
Chunks: make(map[world.ChunkLoc]*world.Chunk), inbound: make(chan pk.Packet, 5),
} }
return
} }
//PlayInfo content player info in server. //PlayInfo content player info in server.
type PlayInfo struct { type PlayInfo struct {
Gamemode int //游戏模式 Gamemode int //游戏模式
Hardcore bool //是否是极限模式 Hardcore bool //是否是极限模式
Dimension int //维度 Dimension int //维度
Difficulty int //难度 Difficulty int //难度
ViewDistance int //视距 ViewDistance int //视距
ReducedDebugInfo bool //减少调试信息 ReducedDebugInfo bool //减少调试信息
WorldName string //当前世界的名字 WorldName string //当前世界的名字
IsDebug bool //调试 IsDebug bool //调试
IsFlat bool //超平坦世界 IsFlat bool //超平坦世界
// SpawnPosition Position //主世界出生点 SpawnPosition Position //主世界出生点
}
// ServInfo contains information about the server implementation.
type ServInfo struct {
Brand string
} }
// PlayerAbilities defines what player can do. // PlayerAbilities defines what player can do.

View File

@ -2,13 +2,38 @@ package bot
import ( import (
"github.com/Tnze/go-mc/bot/world/entity" "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/Tnze/go-mc/chat"
"github.com/google/uuid" "github.com/google/uuid"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
)
type seenPacketFlags uint8
// Valid seenPacketFlags values.
const (
seenJoinGame seenPacketFlags = 1 << iota
seenServerDifficulty
seenPlayerAbilities
seenPlayerInventory
seenUpdateLight
seenChunkData
seenPlayerPositionAndLook
seenSpawnPos
// gameReadyMinPackets are the minimum set of packets that must be seen, for
// the GameReady callback to be invoked.
gameReadyMinPackets = seenJoinGame | seenChunkData | seenUpdateLight |
seenPlayerAbilities | seenPlayerInventory | seenServerDifficulty |
seenPlayerPositionAndLook | seenSpawnPos
) )
type eventBroker struct { type eventBroker struct {
seenPackets seenPacketFlags
isReady bool
GameStart func() error GameStart func() error
ChatMsg func(msg chat.Message, pos byte, sender uuid.UUID) error ChatMsg func(msg chat.Message, pos byte, sender uuid.UUID) error
Disconnect func(reason chat.Message) error Disconnect func(reason chat.Message) error
@ -17,6 +42,7 @@ type eventBroker struct {
SoundPlay func(name string, category int, x, y, z float64, volume, pitch float32) error SoundPlay func(name string, category int, x, y, z float64, volume, pitch float32) error
PluginMessage func(channel string, data []byte) error PluginMessage func(channel string, data []byte) error
HeldItemChange func(slot int) error HeldItemChange func(slot int) error
OpenWindow func(pkt ptypes.OpenWindow) error
// ExperienceChange will be called every time player's experience level updates. // ExperienceChange will be called every time player's experience level updates.
// Parameters: // Parameters:
@ -25,10 +51,39 @@ type eventBroker struct {
// total - total amount of experience received from level 0. // total - total amount of experience received from level 0.
ExperienceChange func(bar float32, level int32, total int32) error ExperienceChange func(bar float32, level int32, total int32) error
WindowsItem func(id byte, slots []entity.Slot) error WindowsItem func(id byte, slots []entity.Slot) error
WindowsItemChange func(id byte, slotID int, slot entity.Slot) error WindowsItemChange func(id byte, slotID int, slot entity.Slot) error
WindowConfirmation func(pkt ptypes.ConfirmTransaction) error
// ReceivePacket will be called when new packet arrive. // ServerDifficultyChange is called whenever the gamemode of the server changes.
// Default handler will run only if pass == false. // At time of writing (1.16.3), difficulty values of 0, 1, 2, and 3 correspond
// to peaceful, easy, normal, and hard respectively.
ServerDifficultyChange func(difficulty int) error
// GameReady is called after the client has joined the server and successfully
// received player state. Additionally, the server has begun sending world
// state (such as lighting and chunk information).
//
// Use this callback as a signal as to when your bot should start 'doing'
// 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) ReceivePacket func(p pk.Packet) (pass bool, err error)
// PrePhysics will be called before a phyiscs tick.
PrePhysics func() error
}
func (b *eventBroker) updateSeenPackets(f seenPacketFlags) error {
b.seenPackets |= f
if (^b.seenPackets)&gameReadyMinPackets == 0 && b.GameReady != nil && !b.isReady {
b.isReady = true
return b.GameReady()
}
return nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,13 @@ import (
"net" "net"
"strconv" "strconv"
"github.com/Tnze/go-mc/data"
mcnet "github.com/Tnze/go-mc/net" mcnet "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
) )
// ProtocolVersion , the protocol version number of minecraft net protocol // ProtocolVersion , the protocol version number of minecraft net protocol
const ProtocolVersion = 751 const ProtocolVersion = 753
// JoinServer connect a Minecraft server for playing the game. // JoinServer connect a Minecraft server for playing the game.
func (c *Client) JoinServer(addr string, port int) (err error) { func (c *Client) JoinServer(addr string, port int) (err error) {
@ -98,7 +99,7 @@ func (c *Client) join(d Dialer, addr string) (err error) {
case 0x02: //Login Success case 0x02: //Login Success
// uuid, l := pk.UnpackString(pack.Data) // uuid, l := pk.UnpackString(pack.Data)
// name, _ := unpackString(pack.Data[l:]) // name, _ := unpackString(pack.Data[l:])
return //switches the connection state to PLAY. return nil
case 0x03: //Set Compression case 0x03: //Set Compression
var threshold pk.VarInt var threshold pk.VarInt
if err := pack.Scan(&threshold); err != nil { if err := pack.Scan(&threshold); err != nil {
@ -124,3 +125,13 @@ type Dialer interface {
func (c *Client) Conn() *mcnet.Conn { func (c *Client) Conn() *mcnet.Conn {
return c.conn return c.conn
} }
// SendMessage sends a chat message.
func (c *Client) SendMessage(msg string) error {
return c.conn.WritePacket(
pk.Marshal(
data.ChatServerbound,
pk.String(msg),
),
)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/Tnze/go-mc/data" "github.com/Tnze/go-mc/data"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
) )
// SwingArm swing player's arm. // SwingArm swing player's arm.
@ -13,7 +14,7 @@ import (
// It's just animation. // It's just animation.
func (c *Client) SwingArm(hand int) error { func (c *Client) SwingArm(hand int) error {
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket(pk.Marshal(
data.AnimationServerbound, data.ArmAnimation,
pk.VarInt(hand), pk.VarInt(hand),
)) ))
} }
@ -21,7 +22,7 @@ func (c *Client) SwingArm(hand int) error {
// Respawn the player when it was dead. // Respawn the player when it was dead.
func (c *Client) Respawn() error { func (c *Client) Respawn() error {
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket(pk.Marshal(
data.ClientStatus, data.ClientCommand,
pk.VarInt(0), pk.VarInt(0),
)) ))
} }
@ -77,18 +78,17 @@ func (c *Client) Chat(msg string) error {
} }
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket(pk.Marshal(
data.ChatMessageServerbound, data.ChatServerbound,
pk.String(msg), pk.String(msg),
)) ))
} }
// PluginMessage is used by mods and plugins to send their data. // PluginMessage is used by mods and plugins to send their data.
func (c *Client) PluginMessage(channal string, msg []byte) error { func (c *Client) PluginMessage(channel string, msg []byte) error {
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket((&ptypes.PluginMessage{
data.PluginMessageServerbound, Channel: pk.Identifier(channel),
pk.Identifier(channal), Data: ptypes.PluginData(msg),
pluginMessageData(msg), }).Encode())
))
} }
// UseBlock is used to place or use a block. // UseBlock is used to place or use a block.
@ -103,7 +103,7 @@ func (c *Client) PluginMessage(channal string, msg []byte) error {
// insideBlock is true when the player's head is inside of a block's collision. // insideBlock is true when the player's head is inside of a block's collision.
func (c *Client) UseBlock(hand, locX, locY, locZ, face int, cursorX, cursorY, cursorZ float32, insideBlock bool) error { func (c *Client) UseBlock(hand, locX, locY, locZ, face int, cursorX, cursorY, cursorZ float32, insideBlock bool) error {
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket(pk.Marshal(
data.PlayerBlockPlacement, data.UseItem,
pk.VarInt(hand), pk.VarInt(hand),
pk.Position{X: locX, Y: locY, Z: locZ}, pk.Position{X: locX, Y: locY, Z: locZ},
pk.VarInt(face), pk.VarInt(face),
@ -120,7 +120,7 @@ func (c *Client) SelectItem(slot int) error {
} }
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket(pk.Marshal(
data.HeldItemChangeServerbound, data.HeldItemSlotServerbound,
pk.Short(slot), pk.Short(slot),
)) ))
} }
@ -144,7 +144,7 @@ func (c *Client) PickItem(slot int) error {
func (c *Client) playerAction(status, locX, locY, locZ, face int) error { func (c *Client) playerAction(status, locX, locY, locZ, face int) error {
return c.conn.WritePacket(pk.Marshal( return c.conn.WritePacket(pk.Marshal(
data.PlayerDigging, data.BlockDig,
pk.VarInt(status), pk.VarInt(status),
pk.Position{X: locX, Y: locY, Z: locZ}, pk.Position{X: locX, Y: locY, Z: locZ},
pk.Byte(face), pk.Byte(face),
@ -180,7 +180,7 @@ func (c *Client) SwapItem() error {
// Disconnect disconnect the server. // Disconnect disconnect the server.
// Server will close the connection. // Server will close the connection.
func (c *Client) Disconnect() error { func (c *Client) disconnect() error {
return c.conn.Close() return c.conn.Close()
} }

118
bot/path/blocks.go Normal file
View File

@ -0,0 +1,118 @@
package path
import (
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
)
var (
safeStepBlocks = make(map[world.BlockStatus]struct{}, 1024)
stepBlocks = []block.Block{
block.Stone,
block.Granite,
block.PolishedGranite,
block.Diorite,
block.PolishedDiorite,
block.Andesite,
block.PolishedAndesite,
block.GrassBlock,
block.GrassPath,
block.Dirt,
block.CoarseDirt,
block.Cobblestone,
block.OakPlanks,
block.SprucePlanks,
block.BirchPlanks,
block.JunglePlanks,
block.AcaciaPlanks,
block.DarkOakPlanks,
block.Bedrock,
block.GoldOre,
block.IronOre,
block.CoalOre,
block.Glass,
block.LapisOre,
block.Sandstone,
block.RedstoneOre,
block.OakStairs,
block.AcaciaStairs,
block.DarkOakStairs,
block.RedSandstoneStairs,
block.PolishedGraniteStairs,
block.SmoothRedSandstoneStairs,
block.MossyStoneBrickStairs,
block.PolishedDioriteStairs,
block.MossyCobblestoneStairs,
block.EndStoneBrickStairs,
block.StoneStairs,
block.SmoothSandstoneStairs,
block.SmoothQuartzStairs,
block.GraniteStairs,
block.AndesiteStairs,
block.RedNetherBrickStairs,
block.PolishedAndesiteStairs,
}
safeWalkBlocks = make(map[world.BlockStatus]struct{}, 128)
walkBlocks = []block.Block{
block.Air,
block.CaveAir,
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,
block.TallGrass,
}
additionalCostBlocks = map[*block.Block]int{
&block.Rail: 120,
&block.PoweredRail: 200,
}
)
func init() {
for _, b := range stepBlocks {
if b.MinStateID == b.MaxStateID {
safeStepBlocks[world.BlockStatus(b.MinStateID)] = struct{}{}
} else {
for i := b.MinStateID; i <= b.MaxStateID; i++ {
safeStepBlocks[world.BlockStatus(i)] = struct{}{}
}
}
}
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
}
func IsLadder(bID world.BlockStatus) bool {
return uint32(bID) >= block.Ladder.MinStateID && uint32(bID) <= block.Ladder.MaxStateID
}

10
bot/path/inputs.go Normal file
View File

@ -0,0 +1,10 @@
package path
// Inputs describes the desired movements of the player.
type Inputs struct {
Yaw, Pitch float64
ThrottleX, ThrottleZ float64
Jump bool
}

358
bot/path/movement.go Normal file
View File

@ -0,0 +1,358 @@
package path
import (
"fmt"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
)
// Cardinal directions.
type Direction uint8
func (d Direction) Offset() (x, y, z int) {
switch d {
case North:
return 0, 0, -1
case South:
return 0, 0, 1
case East:
return 1, 0, 0
case West:
return -1, 0, 0
}
panic(fmt.Sprintf("unknown direction value: %v", d))
}
func (d Direction) Offset2x() (x, y, z int) {
x, y, z = d.Offset()
return x + x, y + y, z + z
}
func (d Direction) String() string {
switch d {
case North:
return "north"
case South:
return "south"
case East:
return "east"
case West:
return "west"
}
return fmt.Sprintf("Direction?<%d>", int(d))
}
// Valid direction values.
const (
North Direction = iota
South
West
East
)
func LadderDirection(bStateID world.BlockStatus) Direction {
return Direction(((uint32(bStateID) - block.Ladder.MinStateID) & 0xE) >> 1)
}
func ChestDirection(bStateID world.BlockStatus) Direction {
return Direction(((uint32(bStateID) - block.Chest.MinStateID) / 6) & 0x3)
}
func StairsDirection(bStateID world.BlockStatus) Direction {
b := block.StateID[uint32(bStateID)]
return Direction(((uint32(bStateID) - block.ByID[b].MinStateID) / 20) & 0x3)
}
// 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,
Drop2North, Drop2South, Drop2East, Drop2West,
AscendNorth, AscendSouth, AscendEast, AscendWest,
DescendLadder, DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest,
AscendLadder,
JumpCrossNorth, JumpCrossSouth, JumpCrossEast, JumpCrossWest,
}
// Valid movement values.
const (
Waypoint Movement = iota
TraverseNorth
TraverseSouth
TraverseEast
TraverseWest
TraverseNorthEast
TraverseNorthWest
TraverseSouthEast
TraverseSouthWest
DropNorth
DropSouth
DropEast
DropWest
Drop2North
Drop2South
Drop2East
Drop2West
AscendNorth
AscendSouth
AscendEast
AscendWest
DescendLadder
DescendLadderNorth
DescendLadderSouth
DescendLadderEast
DescendLadderWest
AscendLadder
JumpCrossEast
JumpCrossWest
JumpCrossNorth
JumpCrossSouth
)
func (m Movement) Possible(nav *Nav, x, y, z int, from V3, previous Movement) 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
}
b1, b2 := nav.World.GetBlockStatus(x, y+1, z), nav.World.GetBlockStatus(x, y+2, z)
u1, u2 := AirLikeBlock(b1) || IsLadder(b1), AirLikeBlock(b2) || IsLadder(b2)
return u1 && u2
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 Drop2North, Drop2South, Drop2East, Drop2West:
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+4, z)) {
return false
}
fallthrough
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))
case DescendLadder, AscendLadder:
if bID := nav.World.GetBlockStatus(x, y+1, z); !AirLikeBlock(bID) && !IsLadder(bID) {
return false
}
return IsLadder(nav.World.GetBlockStatus(x, y, z))
case DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest:
for amt := 0; amt < 2; amt++ {
if bID := nav.World.GetBlockStatus(x, y+amt+1, z); !AirLikeBlock(bID) && !IsLadder(bID) {
return false
}
}
return IsLadder(nav.World.GetBlockStatus(x, y, z))
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
if !AirLikeBlock(nav.World.GetBlockStatus(x, y+1, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+2, z)) || !AirLikeBlock(nav.World.GetBlockStatus(x, y+3, z)) {
return false
}
midX, midZ := (from.X+x)/2, (from.Z+z)/2
if !AirLikeBlock(nav.World.GetBlockStatus(midX, y+1, midZ)) || !AirLikeBlock(nav.World.GetBlockStatus(midX, y+2, midZ)) || !AirLikeBlock(nav.World.GetBlockStatus(midX, y+3, midZ)) {
return false
}
if !AirLikeBlock(nav.World.GetBlockStatus(from.X, from.Y+3, from.Z)) {
return false
}
return SteppableBlock(nav.World.GetBlockStatus(x, y, z))
default:
panic(m)
}
}
func (m Movement) Offset() (x, y, z int) {
switch m {
case Waypoint:
return 0, 0, 0
case TraverseNorth:
return North.Offset()
case TraverseSouth:
return South.Offset()
case TraverseEast:
return East.Offset()
case TraverseWest:
return West.Offset()
case JumpCrossEast:
return East.Offset2x()
case JumpCrossWest:
return West.Offset2x()
case JumpCrossNorth:
return North.Offset2x()
case JumpCrossSouth:
return South.Offset2x()
case AscendLadder:
return 0, 1, 0
case DescendLadder:
return 0, -1, 0
case DropNorth, DescendLadderNorth:
return 0, -1, -1
case DropSouth, DescendLadderSouth:
return 0, -1, 1
case DropEast, DescendLadderEast:
return 1, -1, 0
case DropWest, DescendLadderWest:
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
case Drop2North:
return 0, -2, -1
case Drop2South:
return 0, -2, 1
case Drop2East:
return 1, -2, 0
case Drop2West:
return -1, -2, 0
default:
panic(m)
}
}
func (m Movement) BaseCost() float64 {
switch m {
case Waypoint:
return 0
case TraverseNorth, TraverseSouth, TraverseEast, TraverseWest:
return 2
case TraverseNorthWest, TraverseNorthEast, TraverseSouthWest, TraverseSouthEast:
return 2.5
case DropNorth, DropSouth, DropEast, DropWest, Drop2North, Drop2South, Drop2East, Drop2West:
return 4
case AscendNorth, AscendSouth, AscendEast, AscendWest:
return 4
case DescendLadderNorth, DescendLadderSouth, DescendLadderEast, DescendLadderWest:
return 1.5
case DescendLadder:
return 1
case AscendLadder:
return 3
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
return 5.5
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 Drop2North:
return "drop-2-north"
case Drop2South:
return "drop-2-south"
case Drop2East:
return "drop-2-east"
case Drop2West:
return "drop-2-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"
case DescendLadder:
return "descend-ladder"
case DescendLadderNorth:
return "descend-ladder-north"
case DescendLadderSouth:
return "descend-ladder-south"
case DescendLadderEast:
return "descend-ladder-east"
case DescendLadderWest:
return "descend-ladder-west"
case AscendLadder:
return "ascend-ladder"
case JumpCrossEast:
return "jump-crossing-east"
case JumpCrossWest:
return "jump-crossing-west"
case JumpCrossNorth:
return "jump-crossing-north"
case JumpCrossSouth:
return "jump-crossing-south"
default:
panic(m)
}
}

196
bot/path/path.go Normal file
View File

@ -0,0 +1,196 @@
// Package path implements pathfinding.
package path
import (
"math"
"math/rand"
"strings"
"time"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
"github.com/beefsack/go-astar"
)
// Point represents a point in 3D space.
type Point struct {
X, Y, Z float64
}
type V3 struct {
X, Y, Z int
}
func (v V3) Cost(other V3) float64 {
x, y, z := v.X-other.X, v.Y-other.Y, v.Z-other.Z
return math.Sqrt(float64(x*x+z*z)) + math.Sqrt(1.2*float64(y*y))
}
// Nav represents a navigation to a position.
type Nav struct {
World *world.World
Start, Dest V3
}
func (n *Nav) Path() (path []astar.Pather, distance float64, found bool) {
return astar.Path(
Tile{ // Start point
Nav: n,
Movement: Waypoint,
Pos: n.Start,
},
Tile{ // Destination point
Nav: n,
Movement: Waypoint,
Pos: n.Dest,
})
}
// 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
BlockStatus world.BlockStatus
ExtraCost int
}
func (t Tile) PathNeighborCost(to astar.Pather) float64 {
other := to.(Tile)
return 1 + other.Movement.BaseCost()
}
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 c := t.PathEstimatedCost(Tile{Pos: t.Nav.Start}); c > 8000 {
return nil
}
if t.Pos == t.Nav.Dest && t.Movement != Waypoint {
dupe := t
dupe.Movement = Waypoint
dupe.BlockStatus = 0
return []astar.Pather{dupe}
}
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}
possible := m.Possible(t.Nav, pos.X, pos.Y, pos.Z, t.Pos, t.Movement)
// fmt.Printf("%v-%v: Trying (%v) %v: possible=%v\n", t.Movement, t.Pos, pos, m, possible)
if possible {
possibles = append(possibles, Tile{
Nav: t.Nav,
Movement: m,
Pos: pos,
BlockStatus: t.Nav.World.GetBlockStatus(pos.X, pos.Y, pos.Z),
})
}
}
// fmt.Printf("%v.Neighbours(): %+v\n", t.Pos, possibles)
return possibles
}
func (t Tile) Inputs(pos, deltaPos, vel Point, runTime time.Duration) Inputs {
// Sufficient for simple movements.
at := math.Atan2(-deltaPos.X, -deltaPos.Z)
mdX, _, mdZ := t.Movement.Offset()
wantYaw := -math.Atan2(float64(mdX), float64(mdZ)) * 180 / math.Pi
out := Inputs{
ThrottleX: math.Sin(at),
ThrottleZ: math.Cos(at),
Yaw: wantYaw,
}
if mdX == 0 && mdZ == 0 {
out.Yaw = math.NaN()
}
if (rand.Int() % 14) == 0 {
out.Pitch = float64((rand.Int() % 4) - 2)
}
switch t.Movement {
case DescendLadder, DescendLadderEast, DescendLadderWest, DescendLadderNorth, DescendLadderSouth:
// Deadzone the throttle to prevent an accidental ascend.
if dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z); dist2 < (0.22 * 0.22 * 2) {
out.ThrottleX, out.ThrottleZ = 0, 0
}
case AscendLadder:
dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z)
if x, _, z := LadderDirection(t.BlockStatus).Offset(); dist2 > (0.8*0.8) && deltaPos.Y < 0 {
pos.X -= (0.25 * float64(x))
pos.Z -= (0.25 * float64(z))
} else {
pos.X += (0.42 * float64(x))
pos.Z += (0.42 * float64(z))
}
at = math.Atan2(-pos.X+float64(t.Pos.X)+0.5, -pos.Z+float64(t.Pos.Z)+0.5)
out = Inputs{
ThrottleX: math.Sin(at),
ThrottleZ: math.Cos(at),
Yaw: math.NaN(),
}
case AscendNorth, AscendSouth, AscendEast, AscendWest:
var (
b = block.ByID[block.StateID[uint32(t.BlockStatus)]]
isStairs = strings.HasSuffix(b.Name, "_stairs")
maybeStuck = runTime < 1250*time.Millisecond
dist2 = math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z)
)
out.Jump = dist2 < 1.75 && deltaPos.Y < -0.81
// Special logic for stairs: Try to go towards the downwards edge initially.
if isStairs && dist2 > (0.9*0.9) && deltaPos.Y < 0 {
if x, _, z := StairsDirection(t.BlockStatus).Offset(); dist2 > (0.9*0.9) && deltaPos.Y < 0 {
pos.X += (0.49 * float64(x))
pos.Z += (0.49 * float64(z))
}
at = math.Atan2(-pos.X+float64(t.Pos.X)+0.5, -pos.Z+float64(t.Pos.Z)+0.5)
out = Inputs{
ThrottleX: math.Sin(at),
ThrottleZ: math.Cos(at),
Yaw: math.NaN(),
Jump: out.Jump && !maybeStuck,
}
}
// Turn off the throttle if we get stuck on the jump.
if dist2 < 1 && deltaPos.Y < 0 && vel.Y == 0 {
out.ThrottleX, out.ThrottleZ = 0, 0
}
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
dist2 := math.Sqrt(deltaPos.X*deltaPos.X + deltaPos.Z*deltaPos.Z)
out.Jump = dist2 > 1.5 && dist2 < 1.78
}
return out
}
func (t Tile) IsComplete(d Point) bool {
switch t.Movement {
case DescendLadder, DescendLadderNorth, DescendLadderSouth, DescendLadderWest, DescendLadderEast,
DropNorth, DropSouth, DropEast, DropWest:
return (d.X*d.X+d.Z*d.Z) < (2*0.2*0.25) && d.Y <= 0.05
case AscendLadder:
return d.Y >= 0
case JumpCrossEast, JumpCrossWest, JumpCrossNorth, JumpCrossSouth:
return (d.X*d.X+d.Z*d.Z) < (0.22*0.22) && d.Y >= -0.065
}
return (d.X*d.X+d.Z*d.Z) < (0.18*0.18) && d.Y >= -0.065 && d.Y <= 0.08
}

143
bot/phy/aabb.go Normal file
View File

@ -0,0 +1,143 @@
package phy
import (
"math"
"github.com/Tnze/go-mc/bot/world"
)
type MinMax struct {
Min, Max float64
}
// Extends adjusts the bounds of the MinMax. A negative number will reduce the
// minimum bound, whereas a positive number will increase the maximum bound.
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,
}
}
// Contract reduces both the minimum and maximum bound by the provided amount,
// such that the difference between the bounds decreases for positive values.
func (mm MinMax) Contract(amt float64) MinMax {
return MinMax{
Min: mm.Min + amt,
Max: mm.Max - amt,
}
}
// Expand changes the minimum and maximum bounds by the provided amount, such
// that the difference between the bounds increases for positive values.
func (mm MinMax) Expand(amt float64) MinMax {
return MinMax{
Min: mm.Min - amt,
Max: mm.Max + amt,
}
}
// Offset adds the provided value to both the minimum and maximum value.
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
}
// Extend adjusts the minimum (for negative values) or maximum bounds (for
// positive values) by the provided scalar for each dimension.
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,
}
}
// Contract reduces the difference between the min/max bounds (for positive
// values) for each dimension.
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,
}
}
// Expand increases both the minimum and maximum bounds by the provided amount
// (for positive values) for each dimension.
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,
}
}
// Offset moves both the minimum and maximum bound by the provided value for
// each dimension.
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
}

300
bot/phy/phy.go Normal file
View File

@ -0,0 +1,300 @@
// Package phy implements a minimal physics simulation necessary for realistic
// bot behavior.
package phy
import (
"fmt"
"math"
"github.com/Tnze/go-mc/bot/path"
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/bot/world/entity/player"
"github.com/Tnze/go-mc/data/block/shape"
)
const (
playerWidth = 0.6
playerHeight = 1.8
resetVel = 0.003
maxYawChange = 11
maxPitchChange = 7
stepHeight = 0.6
minJumpTicks = 14
ladderMaxSpeed = 0.15
ladderClimbSpeed = 0.2
gravity = 0.08
drag = 0.98
acceleration = 0.02
inertia = 0.91
slipperiness = 0.6
)
// 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
// State tracks physics state.
type State struct {
// player state.
Pos path.Point
Vel path.Point
Yaw, Pitch float64
lastJump uint32
// player state flags.
onGround bool
collision struct {
vertical bool
horizontal bool
}
tick uint32
Run bool
}
func (s *State) ServerPositionUpdate(player player.Pos, w World) error {
fmt.Printf("TELEPORT (y=%0.2f, velY=%0.3f): %0.2f, %0.2f, %0.2f\n", s.Pos.Y, s.Vel.Y, player.X-s.Pos.X, player.Y-s.Pos.Y, player.Z-s.Pos.Z)
s.Pos = path.Point{X: player.X, Y: player.Y, Z: player.Z}
s.Yaw, s.Pitch = float64(player.Yaw), float64(player.Pitch)
s.Vel = path.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)*2))
for y := minY; y < maxY; y++ {
for z := minZ; z < maxZ; z++ {
for x := minX; x < maxX; x++ {
bStateID := w.GetBlockStatus(x, y, z)
if !path.AirLikeBlock(bStateID) {
bbs, err := shape.CollisionBoxes(bStateID)
if err != nil {
panic(err)
}
for _, box := range bbs {
out = append(out, AABB{
X: MinMax{Min: box.Min.X, Max: box.Max.X},
Y: MinMax{Min: box.Min.Y, Max: box.Max.Y},
Z: MinMax{Min: box.Min.Z, Max: box.Max.Z},
Block: bStateID,
}.Offset(float64(x), float64(y), float64(z)))
}
}
}
}
}
return out
}
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,
}
}
func (s *State) Tick(input path.Inputs, w World) error {
s.tick++
if !s.Run {
return nil
}
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))
}
s.tickVelocity(input, inertia, acceleration, w)
s.tickPosition(w)
if path.IsLadder(w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y)), int(math.Floor(s.Pos.Z)))) && s.collision.horizontal {
s.Vel.Y = ladderClimbSpeed
}
// Gravity
s.Vel.Y -= gravity
// Drag & friction.
s.Vel.Y *= drag
s.Vel.X *= inertia
s.Vel.Z *= inertia
return nil
}
func (s *State) tickVelocity(input path.Inputs, inertia, acceleration float64, w World) {
// 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)
lower := w.GetBlockStatus(int(math.Floor(s.Pos.X)), int(math.Floor(s.Pos.Y)), int(math.Floor(s.Pos.Z)))
if path.IsLadder(lower) {
s.Vel.X = math.Min(math.Max(-ladderMaxSpeed, s.Vel.X), ladderMaxSpeed)
s.Vel.Z = math.Min(math.Max(-ladderMaxSpeed, s.Vel.Z), ladderMaxSpeed)
s.Vel.Y = math.Min(math.Max(-ladderMaxSpeed, s.Vel.Y), ladderMaxSpeed)
}
}
func (s *State) applyLookInputs(input path.Inputs) {
if !math.IsNaN(input.Yaw) {
errYaw := math.Min(math.Max(modYaw(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.onGround {
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) tickPosition(w World) {
// fmt.Printf("TICK POSITION: %0.2f, %0.2f, %0.2f - (%0.2f, %0.2f, %0.2f)\n", s.Pos.X, s.Pos.Y, s.Pos.Z, s.Vel.X, s.Vel.Y, s.Vel.Z)
player, newVel := s.computeCollisionYXZ(s.BB(), s.BB().Offset(s.Vel.X, s.Vel.Y, s.Vel.Z), s.Vel, w)
//fmt.Printf("offset = %0.2f, %0.2f, %0.2f\n", player.X.Min-s.Pos.X, player.Y.Min-s.Pos.Y, player.Z.Min-s.Pos.Z)
//fmt.Printf("onGround = %v, s.Vel.Y = %0.3f, newVel.Y = %0.3f\n", s.onGround, s.Vel.Y, newVel.Y)
if s.onGround || (s.Vel.Y != newVel.Y && s.Vel.Y < 0) {
bb := s.BB()
//fmt.Printf("Player pos = %0.2f, %0.2f, %0.2f\n", bb.X.Min, bb.Y.Min, bb.Z.Min)
surroundings := s.surroundings(bb.Offset(s.Vel.X, stepHeight, s.Vel.Y), w)
outVel := s.Vel
outVel.Y = stepHeight
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)
//fmt.Printf("Post-collision = %0.2f, %0.2f, %0.2f\n", bb.X.Min, bb.Y.Min, bb.Z.Min)
outVel.Y *= -1
// Lower the player back down to be on the ground.
for _, b := range surroundings {
outVel.Y = b.YOffset(bb, outVel.Y)
}
bb = bb.Offset(0, outVel.Y, 0)
//fmt.Printf("Post-lower = %0.2f, %0.2f, %0.2f\n", bb.X.Min, bb.Y.Min, bb.Z.Min)
oldMove := newVel.X*newVel.X + newVel.Z*newVel.Z
newMove := outVel.X*outVel.X + outVel.Z*outVel.Z
// fmt.Printf("oldMove = %0.2f, newMove = %0.2f\n", oldMove*1000, newMove*1000)
if oldMove >= newMove || outVel.Y <= (0.000002-stepHeight) {
// fmt.Println("nope")
} else {
player = bb
newVel = outVel
}
}
// 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
}
func modYaw(new, old float64) float64 {
delta := math.Mod(new-old, 360)
if delta > 180 {
delta = 180 - delta
} else if delta < -180 {
delta += 360
}
// fmt.Printf("(%.2f - %.2f) = %.2f\n", new, old, delta)
return delta
}
func (s *State) computeCollisionYXZ(bb, query AABB, vel path.Point, w World) (outBB AABB, outVel path.Point) {
surroundings := s.surroundings(query, w)
outVel = 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
}
// AtLookTarget returns true if the player look position is actually at the
// given pitch and yaw.
func (s *State) AtLookTarget(yaw, pitch float64) bool {
dYaw, dPitch := math.Abs(modYaw(yaw, s.Yaw)), math.Abs(pitch-s.Pitch)
return dYaw <= 0.8 && dPitch <= 1.1
}

View File

@ -9,6 +9,7 @@ type Settings struct {
DisplayedSkinParts uint8 //皮肤显示 DisplayedSkinParts uint8 //皮肤显示
MainHand int //主手 MainHand int //主手
ReceiveMap bool //接收地图数据 ReceiveMap bool //接收地图数据
Brand string // The brand string presented to the server.
} }
/* /*
@ -33,4 +34,5 @@ var DefaultSettings = Settings{
DisplayedSkinParts: Jacket | LeftSleeve | RightSleeve | LeftPantsLeg | RightPantsLeg | Hat, DisplayedSkinParts: Jacket | LeftSleeve | RightSleeve | LeftPantsLeg | RightPantsLeg | Hat,
MainHand: 1, MainHand: 1,
ReceiveMap: true, ReceiveMap: true,
Brand: "vanilla",
} }

38
bot/world/bitarray.go Normal file
View File

@ -0,0 +1,38 @@
package world
// bitArray implements a bitfield array where values are packed into uint64
// values. If the next value does not fit into remaining space, the remaining
// space of a uint64 is unused.
type bitArray struct {
width uint // bit width of each value
valsPerElement uint // number of values which fit into a single uint64.
data []uint64
}
// Size returns the number of elements that can fit into the bit array.
func (b *bitArray) Size() int {
return int(b.valsPerElement) * len(b.data)
}
func (b *bitArray) Set(idx, val uint) {
var (
arrayIdx = idx / b.valsPerElement
startBit = (idx % b.valsPerElement) * b.width
mask = ^uint64((1<<b.width - 1) << startBit) // set for all bits except target
)
b.data[arrayIdx] = (b.data[arrayIdx] & mask) | uint64(val<<startBit)
}
func (b *bitArray) Get(idx uint) uint {
var (
arrayIdx = idx / b.valsPerElement
offset = (idx % b.valsPerElement) * b.width
mask = uint64((1<<b.width - 1) << offset) // set for just the target
)
return uint(b.data[arrayIdx]&mask) >> offset
}
func valsPerBitArrayElement(bitsPerValue uint) uint {
return uint(64 / bitsPerValue)
}

View File

@ -0,0 +1,50 @@
package world
import (
"encoding/binary"
"encoding/hex"
"testing"
)
func TestBitArrayBasic(t *testing.T) {
a := bitArray{
width: 5,
valsPerElement: valsPerBitArrayElement(5),
data: make([]uint64, 5),
}
if got, want := a.Size(), 12*5; got != want {
t.Errorf("size = %d, want %d", got, want)
}
a.Set(0, 4)
if v := a.Get(0); v != 4 {
t.Errorf("v[0] = %d, want 4", v)
}
a.Set(12, 8)
if v := a.Get(12); v != 8 {
t.Errorf("v[12] = %d, want 8", v)
}
}
func TestBitArrayHardcoded(t *testing.T) {
d1, _ := hex.DecodeString("0020863148418841")
d2, _ := hex.DecodeString("01018A7260F68C87")
a := bitArray{
width: 5,
valsPerElement: valsPerBitArrayElement(5),
data: []uint64{binary.BigEndian.Uint64(d1), binary.BigEndian.Uint64(d2)},
}
if got, want := a.Size(), 12*2; got != want {
t.Errorf("size = %d, want %d", got, want)
}
want := []uint{1, 2, 2, 3, 4, 4, 5, 6, 6, 4, 8, 0, 7, 4, 3, 13, 15, 16, 9, 14, 10, 12, 0, 2}
for idx, want := range want {
if got := a.Get(uint(idx)); got != want {
t.Errorf("v[%d] = %d, want %d", idx, got, want)
}
}
}

View File

@ -3,10 +3,14 @@ package world
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/Tnze/go-mc/data" "math"
"github.com/Tnze/go-mc/data/block"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
) )
const maxPaletteBits = 8
// DecodeChunkColumn decode the chunk data structure. // DecodeChunkColumn decode the chunk data structure.
// If decoding went error, successful decoded data will be returned. // If decoding went error, successful decoded data will be returned.
func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) { func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) {
@ -26,35 +30,35 @@ func DecodeChunkColumn(mask int32, data []byte) (*Chunk, error) {
return &c, nil return &c, nil
} }
func perBits(BitsPerBlock byte) int { func perBits(bpb byte) uint {
switch { switch {
case BitsPerBlock <= 4: case bpb <= 4:
return 4 return 4
case BitsPerBlock < 9: case bpb <= maxPaletteBits:
return int(BitsPerBlock) return uint(bpb)
default: default:
return data.BitsPerBlock // DefaultBitsPerBlock return uint(block.BitsPerBlock)
} }
} }
func readSection(data pk.DecodeReader) (s Section, err error) { func readSection(data pk.DecodeReader) (s Section, err error) {
var BlockCount pk.Short var nonAirBlockCount pk.Short
if err := BlockCount.Decode(data); err != nil { if err := nonAirBlockCount.Decode(data); err != nil {
return nil, fmt.Errorf("read block count error: %w", err) return nil, fmt.Errorf("block count: %w", err)
} }
var bpb pk.UnsignedByte var bpb pk.UnsignedByte
if err := bpb.Decode(data); err != nil { if err := bpb.Decode(data); err != nil {
return nil, fmt.Errorf("read bits per block error: %w", err) return nil, fmt.Errorf("bits per block: %w", err)
} }
// If bpb values greater than or equal to 9, use directSection. // If bpb values greater than or equal to 9, use directSection.
// Otherwise use paletteSection. // Otherwise use paletteSection.
var palettes []BlockStatus var palettes []BlockStatus
var palettesIndex map[BlockStatus]int var palettesIndex map[BlockStatus]int
if bpb < 9 { if bpb <= maxPaletteBits {
// read palettes // read palettes
var length pk.VarInt var length pk.VarInt
if err := length.Decode(data); err != nil { if err := length.Decode(data); err != nil {
return nil, fmt.Errorf("read palettes length error: %w", err) return nil, fmt.Errorf("palette length: %w", err)
} }
palettes = make([]BlockStatus, length) palettes = make([]BlockStatus, length)
palettesIndex = make(map[BlockStatus]int, length) palettesIndex = make(map[BlockStatus]int, length)
@ -85,8 +89,15 @@ func readSection(data pk.DecodeReader) (s Section, err error) {
dataArray[i] = uint64(v) dataArray[i] = uint64(v)
} }
sec := directSection{bpb: perBits(byte(bpb)), data: dataArray} width := perBits(byte(bpb))
if bpb < 9 { sec := directSection{
bitArray{
width: width,
valsPerElement: valsPerBitArrayElement(width),
data: dataArray,
},
}
if bpb <= maxPaletteBits {
return &paletteSection{ return &paletteSection{
palette: palettes, palette: palettes,
palettesIndex: palettesIndex, palettesIndex: palettesIndex,
@ -98,46 +109,38 @@ func readSection(data pk.DecodeReader) (s Section, err error) {
} }
type directSection struct { type directSection struct {
bpb int bitArray
data []uint64
} }
func (d *directSection) GetBlock(offset int) BlockStatus { func (d *directSection) GetBlock(offset uint) BlockStatus {
offset *= d.bpb return BlockStatus(d.Get(offset))
padding := offset % 64
block := uint32(d.data[offset/64] >> padding)
if padding > 64-d.bpb {
l := 64 - padding
block |= uint32(d.data[offset/64+1] << l)
}
return BlockStatus(block & (1<<d.bpb - 1)) // mask
} }
func (d *directSection) SetBlock(offset int, s BlockStatus) { func (d *directSection) SetBlock(offset uint, s BlockStatus) {
offset *= d.bpb d.Set(offset, uint(s))
padding := offset % 64
mask := ^uint64((1<<d.bpb - 1) << padding)
d.data[offset/64] = d.data[offset/64]&mask | uint64(s)<<padding
if padding > 64-d.bpb {
l := padding - (64 - d.bpb)
const maxUint64 = 1<<64 - 1
d.data[offset/64+1] = d.data[offset/64+1]&(maxUint64<<l) | uint64(s)>>(64-padding)
}
} }
func (d *directSection) CanContain(s BlockStatus) bool { func (d *directSection) CanContain(s BlockStatus) bool {
return s <= (1<<d.bpb - 1) return s <= (1<<d.width - 1)
} }
func (d *directSection) clone(bpb int) *directSection { func (d *directSection) clone(bpb uint) *directSection {
newSection := &directSection{ out := newSectionWithSize(bpb)
bpb: bpb, for offset := uint(0); offset < 16*16*16; offset++ {
data: make([]uint64, 16*16*16*bpb/64), out.SetBlock(offset, d.GetBlock(offset))
} }
for offset := 0; offset < 16*16*16; offset++ { return out
newSection.SetBlock(offset, d.GetBlock(offset)) }
func newSectionWithSize(bpb uint) *directSection {
valsPerElement := valsPerBitArrayElement(bpb)
return &directSection{
bitArray{
width: bpb,
valsPerElement: valsPerElement,
data: make([]uint64, int(math.Ceil(16*16*16/float64(valsPerElement)))),
},
} }
return newSection
} }
type paletteSection struct { type paletteSection struct {
@ -146,12 +149,12 @@ type paletteSection struct {
directSection directSection
} }
func (p *paletteSection) GetBlock(offset int) BlockStatus { func (p *paletteSection) GetBlock(offset uint) BlockStatus {
v := p.directSection.GetBlock(offset) v := p.directSection.GetBlock(offset)
return p.palette[v] return p.palette[v]
} }
func (p *paletteSection) SetBlock(offset int, s BlockStatus) { func (p *paletteSection) SetBlock(offset uint, s BlockStatus) {
if i, ok := p.palettesIndex[s]; ok { if i, ok := p.palettesIndex[s]; ok {
p.directSection.SetBlock(offset, BlockStatus(i)) p.directSection.SetBlock(offset, BlockStatus(i))
return return
@ -163,7 +166,7 @@ func (p *paletteSection) SetBlock(offset int, s BlockStatus) {
// Increase the underlying directSection // Increase the underlying directSection
// Suppose that old bpb fit len(p.palette) before it appended. // Suppose that old bpb fit len(p.palette) before it appended.
// So bpb+1 must enough for new len(p.palette). // So bpb+1 must enough for new len(p.palette).
p.directSection = *p.directSection.clone(p.bpb + 1) p.directSection = *p.directSection.clone(p.width + 1)
} }
p.directSection.SetBlock(offset, BlockStatus(i)) p.directSection.SetBlock(offset, BlockStatus(i))
} }

View File

@ -1,78 +0,0 @@
package world
import (
"github.com/Tnze/go-mc/data"
"math/rand"
"testing"
)
func newDirectSection(bpb int) Section {
return &directSection{
bpb: bpb,
data: make([]uint64, 16*16*16*bpb/64),
}
}
func TestDirectSection(t *testing.T) {
for bpb := 4; bpb <= data.BitsPerBlock; bpb++ {
testSection(newDirectSection(bpb), bpb)(t)
}
}
func TestDirectSection_clone(t *testing.T) {
s := newDirectSection(9)
dataset := randData(9)
for i := 0; i < 16*16*16; i++ {
s.SetBlock(i, dataset[i])
}
s = s.(*directSection).clone(data.BitsPerBlock)
for i := 0; i < 16*16*16; i++ {
if s := s.GetBlock(i); dataset[i] != s {
t.Fatalf("direct section error: want: %v, get %v", dataset[i], s)
}
}
}
func TestPaletteSection(t *testing.T) {
t.Run("Correctness", testSection(&paletteSection{
palettesIndex: make(map[BlockStatus]int),
directSection: *(newDirectSection(7).(*directSection)),
}, 7))
t.Run("AutomaticExpansion", testSection(&paletteSection{
palettesIndex: make(map[BlockStatus]int),
directSection: *(newDirectSection(4).(*directSection)),
}, 9))
}
func testSection(s Section, bpb int) func(t *testing.T) {
return func(t *testing.T) {
for _, dataset := range [][16 * 16 * 16]BlockStatus{secData(bpb), randData(bpb)} {
for i := 0; i < 16*16*16; i++ {
s.SetBlock(i, dataset[i])
}
for i := 0; i < 16*16*16; i++ {
if v := s.GetBlock(i); dataset[i] != v {
t.Fatalf("direct section error: want: %v, get %v", dataset[i], v)
}
}
}
}
}
func secData(bpb int) (data [16 * 16 * 16]BlockStatus) {
mask := 1<<bpb - 1
var v int
for i := range data {
data[i] = BlockStatus(v)
v = (v + 1) & mask
}
return
}
func randData(bpb int) (data [16 * 16 * 16]BlockStatus) {
data = secData(bpb)
rand.Shuffle(len(data), func(i, j int) {
data[i], data[j] = data[j], data[i]
})
return
}

View File

@ -1,20 +1,53 @@
package entity package entity
import ( import (
"github.com/Tnze/go-mc/data" "bytes"
"github.com/Tnze/go-mc/data/entity"
item "github.com/Tnze/go-mc/data/item"
"github.com/Tnze/go-mc/nbt" "github.com/Tnze/go-mc/nbt"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
) )
//Entity is the entity of minecraft // BlockEntity describes the representation of a tile entity at a position.
type BlockEntity struct {
ID string `nbt:"id"`
// global co-ordinates
X int `nbt:"x"`
Y int `nbt:"y"`
Z int `nbt:"z"`
// sign-specific.
Color string `nbt:"color"`
Text1 string `nbt:"Text1"`
Text2 string `nbt:"Text2"`
Text3 string `nbt:"Text3"`
Text4 string `nbt:"Text4"`
}
//Entity represents an instance of an entity.
type Entity struct { type Entity struct {
EntityID int //实体ID ID int32
Data int32
Base *entity.Entity
UUID uuid.UUID
X, Y, Z float64
Pitch, Yaw int8
VelX, VelY, VelZ int16
OnGround bool
IsLiving bool
HeadPitch int8
} }
// The Slot data structure is how Minecraft represents an item and its associated data in the Minecraft Protocol // The Slot data structure is how Minecraft represents an item and its associated data in the Minecraft Protocol
type Slot struct { type Slot struct {
Present bool Present bool
ItemID int32 ItemID item.ID
Count int8 Count int8
NBT interface{} NBT interface{}
} }
@ -25,9 +58,11 @@ func (s *Slot) Decode(r pk.DecodeReader) error {
return err return err
} }
if s.Present { if s.Present {
if err := (*pk.VarInt)(&s.ItemID).Decode(r); err != nil { var itemID pk.VarInt
if err := itemID.Decode(r); err != nil {
return err return err
} }
s.ItemID = item.ID(itemID)
if err := (*pk.Byte)(&s.Count).Decode(r); err != nil { if err := (*pk.Byte)(&s.Count).Decode(r); err != nil {
return err return err
} }
@ -38,6 +73,25 @@ func (s *Slot) Decode(r pk.DecodeReader) error {
return nil return nil
} }
func (s Slot) String() string { func (s Slot) Encode() []byte {
return data.ItemNameByID[s.ItemID] if !s.Present {
return pk.Boolean(false).Encode()
}
var b bytes.Buffer
b.Write(pk.Boolean(true).Encode())
b.Write(pk.VarInt(s.ItemID).Encode())
b.Write(pk.Byte(s.Count).Encode())
if s.NBT != nil {
nbt.NewEncoder(&b).Encode(s.NBT)
} else {
b.Write([]byte{nbt.TagEnd})
}
return b.Bytes()
}
func (s Slot) String() string {
return item.ByID[item.ID(s.ItemID)].DisplayName
} }

View File

@ -2,14 +2,28 @@ package player
import "github.com/Tnze/go-mc/bot/world/entity" 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. // Player includes the player's status.
type Player struct { type Player struct {
entity.Entity entity.Entity
UUID [2]int64 //128bit UUID UUID [2]int64 //128bit UUID
X, Y, Z float64 Pos Pos
Yaw, Pitch float32
OnGround bool
HeldItem int //拿着的物品栏位 HeldItem int //拿着的物品栏位

21
bot/world/tile.go Normal file
View File

@ -0,0 +1,21 @@
package world
import (
"fmt"
)
// TilePosition describes the location of a tile/block entity within a chunk.
type TilePosition uint32
func (p TilePosition) Pos() (x, y, z int) {
return int((p>>8) & 0xff), int((p>>16) & 0xff), int(p&0xff)
}
func (p TilePosition) String() string {
x, y, z := p.Pos()
return fmt.Sprintf("(%d, %d, %d)", x, y, z)
}
func ToTilePos(x, y, z int) TilePosition {
return TilePosition((y&0xff) << 16 | (x&15) << 8 | (z&15))
}

View File

@ -1,77 +1,15 @@
package world package world
import ( import (
"sync"
"github.com/Tnze/go-mc/bot/world/entity" "github.com/Tnze/go-mc/bot/world/entity"
) )
// World record all of the things in the world where player at // World record all of the things in the world where player at
type World struct { type World struct {
Entities map[int32]entity.Entity entityLock sync.RWMutex
Chunks map[ChunkLoc]*Chunk Entities map[int32]*entity.Entity
} chunkLock sync.RWMutex
Chunks map[ChunkLoc]*Chunk
// Chunk store a 256*16*16 column blocks
type Chunk struct {
Sections [16]Section
}
// Section store a 16*16*16 cube blocks
type Section interface {
// GetBlock return block status, offset can be calculate by SectionOffset.
GetBlock(offset int) BlockStatus
// SetBlock is the reverse operation of GetBlock.
SetBlock(offset int, s BlockStatus)
}
func SectionOffset(x, y, z int) (offset int) {
// According to wiki.vg: Data Array is given for each block with increasing x coordinates,
// within rows of increasing z coordinates, within layers of increasing y coordinates.
// So offset equals to ( x*16^0 + z*16^1 + y*16^2 )*(bits per block).
return x + z*16 + y*16*16
}
type BlockStatus uint32
type ChunkLoc struct {
X, Z int
}
// //Entity 表示一个实体
// type Entity interface {
// EntityID() int32
// }
// //Face is a face of a block
// type Face byte
// // All six faces in a block
// const (
// Bottom Face = iota
// Top
// North
// South
// West
// East
// )
// getBlock return the block in the position (x, y, z)
func (w *World) GetBlockStatus(x, y, z int) BlockStatus {
// Use n>>4 rather then n/16. It acts wrong if n<0.
c := w.Chunks[ChunkLoc{x >> 4, z >> 4}]
if c != nil {
// (n&(16-1)) == (n<0 ? n%16+16 : n%16)
if sec := c.Sections[y>>4]; sec != nil {
return sec.GetBlock(SectionOffset(x&15, y&15, z&15))
}
}
return 0
}
// func (b Block) String() string {
// return blockNameByID[b.id]
// }
//LoadChunk load chunk at (x, z)
func (w *World) LoadChunk(x, z int, c *Chunk) {
w.Chunks[ChunkLoc{X: x, Z: z}] = c
} }

152
bot/world/world_chunk.go Normal file
View File

@ -0,0 +1,152 @@
package world
import (
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/data/block"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
)
// Chunk store a 256*16*16 area of blocks, sharded on the Y axis into 16
// sections.
type Chunk struct {
Sections [16]Section
TileEntities map[TilePosition]entity.BlockEntity
}
// Section implements storage of blocks within a fixed 16*16*16 area.
type Section interface {
// GetBlock return block status, offset can be calculate by SectionOffset.
GetBlock(offset uint) BlockStatus
// SetBlock is the reverse operation of GetBlock.
SetBlock(offset uint, s BlockStatus)
}
func sectionIdx(x, y, z int) uint {
return uint(((y & 15) << 8) | (z << 4) | x)
}
type BlockStatus uint32
type ChunkLoc struct {
X, Z int
}
// Signs returns a list of signs on the server within viewing range.
func (w *World) Signs() []entity.BlockEntity {
w.chunkLock.RLock()
defer w.chunkLock.RUnlock()
out := make([]entity.BlockEntity, 0, 4)
for _, c := range w.Chunks {
for _, e := range c.TileEntities {
if e.ID == "minecraft:sign" {
out = append(out, e)
}
}
}
return out
}
// GetBlockStatus return the state ID of the block at the given position.
func (w *World) GetBlockStatus(x, y, z int) BlockStatus {
w.chunkLock.RLock()
defer w.chunkLock.RUnlock()
c := w.Chunks[ChunkLoc{x >> 4, z >> 4}]
if c != nil && y >= 0 {
if sec := c.Sections[y>>4]; sec != nil {
return sec.GetBlock(sectionIdx(x&15, y&15, z&15))
}
}
return 0
}
// UnloadChunk unloads a chunk from the world.
func (w *World) UnloadChunk(loc ChunkLoc) {
w.chunkLock.Lock()
delete(w.Chunks, loc)
w.chunkLock.Unlock()
}
// UnaryBlockUpdate updates the block at the specified position with a new
// state ID.
func (w *World) UnaryBlockUpdate(pos pk.Position, bStateID BlockStatus) bool {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
c := w.Chunks[ChunkLoc{X: pos.X >> 4, Z: pos.Z >> 4}]
if c == nil {
return false
}
sIdx, bIdx := pos.Y>>4, sectionIdx(pos.X&15, pos.Y&15, pos.Z&15)
if sec := c.Sections[sIdx]; sec == nil {
sec = newSectionWithSize(uint(block.BitsPerBlock))
sec.SetBlock(bIdx, bStateID)
c.Sections[sIdx] = sec
} else {
tp := ToTilePos(pos.X, pos.Y, pos.Z)
if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID {
delete(c.TileEntities, tp)
}
sec.SetBlock(bIdx, bStateID)
}
return true
}
// MultiBlockUpdate updates a packed specification of blocks within a single
// section of a chunk.
func (w *World) MultiBlockUpdate(loc ChunkLoc, sectionY int, blocks []pk.VarLong) bool {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
c := w.Chunks[loc]
if c == nil {
return false // not loaded
}
sec := c.Sections[sectionY]
if sec == nil {
sec = newSectionWithSize(uint(block.BitsPerBlock))
c.Sections[sectionY] = sec
}
for _, b := range blocks {
var (
bStateID = BlockStatus(b >> 12)
x, z, y = (b >> 8) & 0xf, (b >> 4) & 0xf, b & 0xf
bIdx = sectionIdx(int(x&15), int(y&15), int(z&15))
tp = ToTilePos(int(x), int(y), int(z))
)
if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID {
delete(c.TileEntities, tp)
}
sec.SetBlock(bIdx, bStateID)
}
return true
}
//LoadChunk adds the given chunk to the world.
func (w *World) LoadChunk(x, z int, c *Chunk) {
w.chunkLock.Lock()
w.Chunks[ChunkLoc{X: x, Z: z}] = c
w.chunkLock.Unlock()
}
func (w *World) TileEntityUpdate(pkt ptypes.TileEntityData) error {
w.chunkLock.Lock()
defer w.chunkLock.Unlock()
c := w.Chunks[ChunkLoc{X: pkt.Pos.X >> 4, Z: pkt.Pos.Z >> 4}]
if c == nil {
return nil
}
switch pkt.Action {
case 9: // Sign update
c.TileEntities[ToTilePos(pkt.Pos.X, pkt.Pos.Y, pkt.Pos.Z)] = pkt.Data
}
return nil
}

167
bot/world/world_entity.go Normal file
View File

@ -0,0 +1,167 @@
package world
import (
"fmt"
"github.com/Tnze/go-mc/bot/world/entity"
e "github.com/Tnze/go-mc/data/entity"
pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/net/ptypes"
"github.com/google/uuid"
)
// PlayerEntities returns a list of players on the server within viewing range.
func (w *World) PlayerEntities() []entity.Entity {
w.entityLock.RLock()
defer w.entityLock.RUnlock()
out := make([]entity.Entity, 0, 12)
for _, ent := range w.Entities {
if ent.Base.ID == e.Player.ID {
out = append(out, *ent)
}
}
return out
}
// OnSpawnEntity should be called when a SpawnEntity packet
// is recieved.
func (w *World) OnSpawnEntity(pkt ptypes.SpawnEntity) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
base, ok := e.ByID[e.ID(pkt.Type)]
if !ok {
return fmt.Errorf("unknown entity ID %v", pkt.Type)
}
w.Entities[int32(pkt.ID)] = &entity.Entity{
ID: int32(pkt.ID),
Base: base,
Data: int32(pkt.Data),
UUID: uuid.UUID(pkt.UUID),
X: float64(pkt.X),
Y: float64(pkt.Y),
Z: float64(pkt.Z),
Pitch: int8(pkt.Pitch),
Yaw: int8(pkt.Yaw),
VelX: int16(pkt.VelX),
VelY: int16(pkt.VelY),
VelZ: int16(pkt.VelZ),
}
return nil
}
// OnSpawnLivingEntity should be called when a SpawnLivingEntity packet
// is recieved.
func (w *World) OnSpawnLivingEntity(pkt ptypes.SpawnLivingEntity) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
base, ok := e.ByID[e.ID(pkt.Type)]
if !ok {
return fmt.Errorf("unknown entity ID %v", pkt.Type)
}
// fmt.Printf("SpawnLivingEntity %q\n", base.Name)
w.Entities[int32(pkt.ID)] = &entity.Entity{
ID: int32(pkt.ID),
Base: base,
UUID: uuid.UUID(pkt.UUID),
X: float64(pkt.X),
Y: float64(pkt.Y),
Z: float64(pkt.Z),
Pitch: int8(pkt.Pitch),
Yaw: int8(pkt.Yaw),
VelX: int16(pkt.VelX),
VelY: int16(pkt.VelY),
VelZ: int16(pkt.VelZ),
HeadPitch: int8(pkt.HeadPitch),
}
return nil
}
// OnSpawnPlayer should be called when a SpawnPlayer packet
// is recieved.
func (w *World) OnSpawnPlayer(pkt ptypes.SpawnPlayer) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
// fmt.Printf("SpawnPlayer %v\n", pkt)
w.Entities[int32(pkt.ID)] = &entity.Entity{
ID: int32(pkt.ID),
Base: &e.Player,
UUID: uuid.UUID(pkt.UUID),
X: float64(pkt.X),
Y: float64(pkt.Y),
Z: float64(pkt.Z),
Pitch: int8(pkt.Pitch),
Yaw: int8(pkt.Yaw),
}
return nil
}
// OnEntityPosUpdate should be called when an EntityPosition packet
// is recieved.
func (w *World) OnEntityPosUpdate(pkt ptypes.EntityPosition) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
ent, ok := w.Entities[int32(pkt.ID)]
if !ok {
return fmt.Errorf("cannot handle position update for unknown entity %d", pkt.ID)
}
ent.X += float64(pkt.X) / (128 * 32)
ent.Y += float64(pkt.Y) / (128 * 32)
ent.Z += float64(pkt.Z) / (128 * 32)
ent.OnGround = bool(pkt.OnGround)
return nil
}
// OnEntityPosLookUpdate should be called when an EntityPositionLook packet
// is recieved.
func (w *World) OnEntityPosLookUpdate(pkt ptypes.EntityPositionLook) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
ent, ok := w.Entities[int32(pkt.ID)]
if !ok {
return fmt.Errorf("cannot handle position look update for unknown entity %d", pkt.ID)
}
ent.X += float64(pkt.X) / (128 * 32)
ent.Y += float64(pkt.Y) / (128 * 32)
ent.Z += float64(pkt.Z) / (128 * 32)
ent.OnGround = bool(pkt.OnGround)
ent.Pitch, ent.Yaw = int8(pkt.Pitch), int8(pkt.Yaw)
return nil
}
// OnEntityLookUpdate should be called when an EntityRotation packet
// is recieved.
func (w *World) OnEntityLookUpdate(pkt ptypes.EntityRotation) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
ent, ok := w.Entities[int32(pkt.ID)]
if !ok {
return fmt.Errorf("cannot handle look update for unknown entity %d", pkt.ID)
}
ent.Pitch, ent.Yaw = int8(pkt.Pitch), int8(pkt.Yaw)
ent.OnGround = bool(pkt.OnGround)
return nil
}
// OnEntityDestroy should be called when a DestroyEntities packet
// is recieved.
func (w *World) OnEntityDestroy(eIDs []pk.VarInt) error {
w.entityLock.Lock()
defer w.entityLock.Unlock()
for _, eID := range eIDs {
delete(w.Entities, int32(eID))
}
return nil
}

View File

@ -1,10 +1,11 @@
package main package main
import ( import (
"github.com/google/uuid"
"log" "log"
"time" "time"
"github.com/google/uuid"
"github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/chat"
_ "github.com/Tnze/go-mc/data/lang/en-us" _ "github.com/Tnze/go-mc/data/lang/en-us"

View File

@ -2,9 +2,10 @@ package main
import ( import (
"bytes" "bytes"
"github.com/google/uuid"
"log" "log"
"github.com/google/uuid"
"github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/chat"
_ "github.com/Tnze/go-mc/data/lang/en-us" _ "github.com/Tnze/go-mc/data/lang/en-us"

View File

@ -3,8 +3,9 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/Tnze/go-mc/yggdrasil"
"os" "os"
"github.com/Tnze/go-mc/yggdrasil"
) )
var user = flag.String("user", "", "Can be an email address or player name for unmigrated accounts") var user = flag.String("user", "", "Can be an email address or player name for unmigrated accounts")

View File

@ -2,12 +2,13 @@
package main package main
import ( import (
"log"
"github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/data" "github.com/Tnze/go-mc/data"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid" "github.com/google/uuid"
"log"
) )
const ProtocolVersion = 578 const ProtocolVersion = 578
@ -64,7 +65,7 @@ func handlePlaying(conn net.Conn, protocol int32) {
} }
joinGame(conn) joinGame(conn)
conn.WritePacket(pk.Marshal(data.PlayerPositionAndLookClientbound, conn.WritePacket(pk.Marshal(data.PositionClientbound,
// https://wiki.vg/Protocol#Player_Position_And_Look_.28clientbound.29 // https://wiki.vg/Protocol#Player_Position_And_Look_.28clientbound.29
pk.Double(0), pk.Double(0), pk.Double(0), // XYZ pk.Double(0), pk.Double(0), pk.Double(0), // XYZ
pk.Float(0), pk.Float(0), // Yaw Pitch pk.Float(0), pk.Float(0), // Yaw Pitch
@ -139,7 +140,7 @@ func loginSuccess(conn net.Conn, name string, uuid uuid.UUID) error {
} }
func joinGame(conn net.Conn) error { func joinGame(conn net.Conn) error {
return conn.WritePacket(pk.Marshal(data.JoinGame, return conn.WritePacket(pk.Marshal(data.Login,
pk.Int(0), // EntityID pk.Int(0), // EntityID
pk.UnsignedByte(1), // Gamemode pk.UnsignedByte(1), // Gamemode
pk.Int(0), // Dimension pk.Int(0), // Dimension

18681
data/block/block.go Normal file

File diff suppressed because it is too large Load Diff

199
data/block/gen_blocks.go Normal file
View File

@ -0,0 +1,199 @@
// gen_blocks.go generates block information.
//+build ignore
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/format"
"go/token"
"net/http"
"os"
"reflect"
"strconv"
"github.com/iancoleman/strcase"
)
const (
infoURL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.16.2/blocks.json"
)
type Block struct {
ID uint32 `json:"id"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
Hardness float64 `json:"hardness"`
Diggable bool `json:"diggable"`
DropIDs []uint32 `json:"drops"`
NeedsTools map[uint32]bool `json:"harvestTools"`
MinStateID uint32 `json:"minStateId"`
MaxStateID uint32 `json:"maxStateId"`
Transparent bool `json:"transparent"`
FilterLightLevel int `json:"filterLight"`
EmitLightLevel int `json:"emitLight"`
}
func downloadInfo() ([]Block, error) {
resp, err := http.Get(infoURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data []Block
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return data, nil
}
func makeBlockDeclaration(blocks []Block) *ast.DeclStmt {
out := &ast.DeclStmt{Decl: &ast.GenDecl{Tok: token.VAR}}
for _, b := range blocks {
t := reflect.TypeOf(b)
fields := make([]ast.Expr, t.NumField())
for i := 0; i < t.NumField(); i++ {
ft := t.Field(i)
var val ast.Expr
switch ft.Type.Kind() {
case reflect.Uint32, reflect.Int:
val = &ast.BasicLit{Kind: token.INT, Value: fmt.Sprint(reflect.ValueOf(b).Field(i))}
case reflect.Float64:
val = &ast.BasicLit{Kind: token.FLOAT, Value: fmt.Sprint(reflect.ValueOf(b).Field(i))}
case reflect.String:
val = &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(reflect.ValueOf(b).Field(i).String())}
case reflect.Bool:
val = &ast.BasicLit{Kind: token.IDENT, Value: fmt.Sprint(reflect.ValueOf(b).Field(i).Bool())}
case reflect.Slice:
val = &ast.CompositeLit{
Type: &ast.ArrayType{
Elt: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Elem().Name()},
},
}
v := reflect.ValueOf(b).Field(i)
switch ft.Type.Elem().Kind() {
case reflect.Uint32, reflect.Int:
for x := 0; x < v.Len(); x++ {
val.(*ast.CompositeLit).Elts = append(val.(*ast.CompositeLit).Elts, &ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprint(v.Index(x)),
})
}
}
case reflect.Map:
// Must be the NeedsTools map of type map[uint32]bool.
m := &ast.CompositeLit{
Type: &ast.MapType{
Key: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Key().Name()},
Value: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Elem().Name()},
},
}
iter := reflect.ValueOf(b).Field(i).MapRange()
for iter.Next() {
m.Elts = append(m.Elts, &ast.KeyValueExpr{
Key: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprint(iter.Key().Uint())},
Value: &ast.BasicLit{Kind: token.IDENT, Value: fmt.Sprint(iter.Value().Bool())},
})
}
val = m
}
fields[i] = &ast.KeyValueExpr{
Key: &ast.Ident{Name: ft.Name},
Value: val,
}
}
out.Decl.(*ast.GenDecl).Specs = append(out.Decl.(*ast.GenDecl).Specs, &ast.ValueSpec{
Names: []*ast.Ident{{Name: strcase.ToCamel(b.Name)}},
Values: []ast.Expr{
&ast.CompositeLit{
Type: &ast.Ident{Name: reflect.TypeOf(b).Name()},
Elts: fields,
},
},
})
}
return out
}
func main() {
blocks, err := downloadInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(`// Package block stores information about blocks in Minecraft.
package block
import (
"math"
)
// BitsPerBlock indicates how many bits are needed to represent all possible
// block states. This value is used to determine the size of the global palette.
var BitsPerBlock = int(math.Ceil(math.Log2(float64(len(StateID)))))
// ID describes the numeric ID of a block.
type ID uint32
// Block describes information about a type of block.
type Block struct {
ID ID
DisplayName string
Name string
Hardness float64
Diggable bool
DropIDs []uint32
NeedsTools map[uint32]bool
MinStateID uint32
MaxStateID uint32
Transparent bool
FilterLightLevel int
EmitLightLevel int
}
`)
format.Node(os.Stdout, token.NewFileSet(), makeBlockDeclaration(blocks))
fmt.Println()
fmt.Println()
fmt.Println("// ByID is an index of minecraft blocks by their ID.")
fmt.Println("var ByID = map[ID]*Block{")
for _, b := range blocks {
fmt.Printf(" %d: &%s,\n", b.ID, strcase.ToCamel(b.Name))
}
fmt.Println("}")
fmt.Println()
fmt.Println("// StateID maps all possible state IDs to a corresponding block ID.")
fmt.Println("var StateID = map[uint32]ID{")
for _, b := range blocks {
if b.MinStateID == b.MaxStateID {
fmt.Printf(" %d: %d,\n", b.MinStateID, b.ID)
} else {
for i := b.MinStateID; i <= b.MaxStateID; i++ {
fmt.Printf(" %d: %d,\n", i, b.ID)
}
}
}
fmt.Println("}")
}

View File

@ -0,0 +1,132 @@
// gen_shape.go generates block shape information.
//+build ignore
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/iancoleman/strcase"
)
const (
infoURL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.16.1/blockCollisionShapes.json"
)
func downloadInfo() (map[string]interface{}, error) {
resp, err := http.Get(infoURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return data, nil
}
func main() {
info, err := downloadInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(`// Package shape stores information about the shapes of blocks in Minecraft.
package shape
import (
"github.com/Tnze/go-mc/bot/world"
"github.com/Tnze/go-mc/data/block"
)
// ID describes a numeric shape ID.
type ID uint32
// Shape describes how collisions should be tested for an object.
type Shape struct {
ID ID
Boxes []BoundingBox
}
type BoundingTriplet struct {
X, Y, Z float64
}
type BoundingBox struct {
Min,Max BoundingTriplet
}
// CollisionBoxes returns the set of bounding boxes for that block state ID.
func CollisionBoxes(bStateID world.BlockStatus) ([]BoundingBox, error) {
bID := block.StateID[uint32(bStateID)]
if bID == 0 {
return nil, fmt.Errorf("unknown state ID: %v", bStateID)
}
b, ok := block.ByID[bID]
if !ok {
return nil, fmt.Errorf("unknown block ID: %v", bID)
}
shapes, ok := ByBlockID[bID]
if !ok {
return nil, fmt.Errorf("unknown shape for block ID: %v", bID)
}
shapeIdx := (uint32(bStateID) - b.MinStateID) % uint32(len(shapes))
if int(shapeIdx) > len(shapes) {
return nil, fmt.Errorf("shape index out of bounds: %v >= %v", shapeIdx, len(shapes))
}
return Dimensions[shapes[shapeIdx]].Boxes, nil
}
`)
fmt.Println()
fmt.Println()
fmt.Println("// ByBlockID is an index of shapes for each minecraft block variant.")
fmt.Println("var ByBlockID = map[block.ID][]ID{")
blocks := info["blocks"].(map[string]interface{})
for name, shapes := range blocks {
switch s := shapes.(type) {
case []interface{}:
set := make([]string, len(s))
for i := range s {
set[i] = fmt.Sprint(s[i])
}
fmt.Printf(" block.%s.ID: []ID{%s},\n", strcase.ToCamel(name), strings.Join(set, ", "))
default:
fmt.Printf(" block.%s.ID: []ID{%s},\n", strcase.ToCamel(name), fmt.Sprint(s))
}
}
fmt.Println("}")
fmt.Println()
fmt.Println()
fmt.Println("// Dimensions describes the bounding boxes of a shape ID.")
fmt.Println("var Dimensions = map[ID]Shape{")
shapes := info["shapes"].(map[string]interface{})
for id, boxes := range shapes {
fmt.Printf(" %s: Shape{\n", id)
fmt.Printf(" ID: %s,\n", id)
fmt.Printf(" Boxes: []BoundingBox{\n")
for _, box := range boxes.([]interface{}) {
elements := box.([]interface{})
if len(elements) != 6 {
panic("expected 6 elements")
}
fmt.Printf(" {\n")
fmt.Printf(" Min: BoundingTriplet{X: %v, Y: %v, Z: %v},\n", elements[0], elements[1], elements[2])
fmt.Printf(" Max: BoundingTriplet{X: %v, Y: %v, Z: %v},\n", elements[3], elements[4], elements[5])
fmt.Printf(" },\n")
}
fmt.Printf(" },\n")
fmt.Println(" },")
}
fmt.Println("}")
}

4696
data/block/shape/shape.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

257
data/entity/entity.go Normal file
View File

@ -0,0 +1,257 @@
// Package entity stores information about entities in Minecraft.
package entity
// ID describes the numeric ID of an entity.
type ID uint32
// Category groups like entities.
type Category uint8
// Valid entity categories.
const (
Unknown Category = iota
Blocks
Immobile
Vehicles
Drops
Projectiles
PassiveMob
HostileMob
)
// Entity describes information about a type of entity.
type Entity struct {
ID ID
InternalID uint32
DisplayName string
Name string
Width float64
Height float64
Type string
Category Category
}
var (
AreaEffectCloud = Entity{ID: 0, InternalID: 0, DisplayName: "Area Effect Cloud", Name: "area_effect_cloud", Width: 6, Height: 0.5, Type: "mob", Category: Immobile}
ArmorStand = Entity{ID: 1, InternalID: 1, DisplayName: "Armor Stand", Name: "armor_stand", Width: 0.5, Height: 1.975, Type: "mob", Category: Immobile}
Arrow = Entity{ID: 2, InternalID: 2, DisplayName: "Arrow", Name: "arrow", Width: 0.5, Height: 0.5, Type: "mob", Category: Projectiles}
Bat = Entity{ID: 3, InternalID: 3, DisplayName: "Bat", Name: "bat", Width: 0.5, Height: 0.9, Type: "mob", Category: PassiveMob}
Bee = Entity{ID: 4, InternalID: 4, DisplayName: "Bee", Name: "bee", Width: 0.7, Height: 0.6, Type: "UNKNOWN", Category: Unknown}
Blaze = Entity{ID: 5, InternalID: 5, DisplayName: "Blaze", Name: "blaze", Width: 0.6, Height: 1.8, Type: "mob", Category: HostileMob}
Boat = Entity{ID: 6, InternalID: 6, DisplayName: "Boat", Name: "boat", Width: 1.375, Height: 0.5625, Type: "mob", Category: Vehicles}
Cat = Entity{ID: 7, InternalID: 7, DisplayName: "Cat", Name: "cat", Width: 0.6, Height: 0.7, Type: "mob", Category: PassiveMob}
CaveSpider = Entity{ID: 8, InternalID: 8, DisplayName: "Cave Spider", Name: "cave_spider", Width: 0.7, Height: 0.5, Type: "mob", Category: HostileMob}
Chicken = Entity{ID: 9, InternalID: 9, DisplayName: "Chicken", Name: "chicken", Width: 0.4, Height: 0.7, Type: "mob", Category: PassiveMob}
Cod = Entity{ID: 10, InternalID: 10, DisplayName: "Cod", Name: "cod", Width: 0.5, Height: 0.3, Type: "mob", Category: PassiveMob}
Cow = Entity{ID: 11, InternalID: 11, DisplayName: "Cow", Name: "cow", Width: 0.9, Height: 1.4, Type: "mob", Category: PassiveMob}
Creeper = Entity{ID: 12, InternalID: 12, DisplayName: "Creeper", Name: "creeper", Width: 0.6, Height: 1.7, Type: "mob", Category: HostileMob}
Dolphin = Entity{ID: 13, InternalID: 13, DisplayName: "Dolphin", Name: "dolphin", Width: 0.9, Height: 0.6, Type: "mob", Category: PassiveMob}
Donkey = Entity{ID: 14, InternalID: 14, DisplayName: "Donkey", Name: "donkey", Width: 1.39648, Height: 1.5, Type: "mob", Category: PassiveMob}
DragonFireball = Entity{ID: 15, InternalID: 15, DisplayName: "Dragon Fireball", Name: "dragon_fireball", Width: 1, Height: 1, Type: "mob", Category: Projectiles}
Drowned = Entity{ID: 16, InternalID: 16, DisplayName: "Drowned", Name: "drowned", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
ElderGuardian = Entity{ID: 17, InternalID: 17, DisplayName: "Elder Guardian", Name: "elder_guardian", Width: 1.9975, Height: 1.9975, Type: "mob", Category: HostileMob}
EndCrystal = Entity{ID: 18, InternalID: 18, DisplayName: "End Crystal", Name: "end_crystal", Width: 2, Height: 2, Type: "mob", Category: Unknown}
EnderDragon = Entity{ID: 19, InternalID: 19, DisplayName: "Ender Dragon", Name: "ender_dragon", Width: 16, Height: 8, Type: "mob", Category: HostileMob}
Enderman = Entity{ID: 20, InternalID: 20, DisplayName: "Enderman", Name: "enderman", Width: 0.6, Height: 2.9, Type: "mob", Category: HostileMob}
Endermite = Entity{ID: 21, InternalID: 21, DisplayName: "Endermite", Name: "endermite", Width: 0.4, Height: 0.3, Type: "mob", Category: HostileMob}
Evoker = Entity{ID: 22, InternalID: 22, DisplayName: "Evoker", Name: "evoker", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
EvokerFangs = Entity{ID: 23, InternalID: 23, DisplayName: "Evoker Fangs", Name: "evoker_fangs", Width: 0.5, Height: 0.8, Type: "mob", Category: HostileMob}
ExperienceOrb = Entity{ID: 24, InternalID: 24, DisplayName: "Experience Orb", Name: "experience_orb", Width: 0.5, Height: 0.5, Type: "mob", Category: Unknown}
EyeOfEnder = Entity{ID: 25, InternalID: 25, DisplayName: "Eye of Ender", Name: "eye_of_ender", Width: 0.25, Height: 0.25, Type: "mob", Category: Unknown}
FallingBlock = Entity{ID: 26, InternalID: 26, DisplayName: "Falling Block", Name: "falling_block", Width: 0.98, Height: 0.98, Type: "mob", Category: Blocks}
FireworkRocket = Entity{ID: 27, InternalID: 27, DisplayName: "Firework Rocket", Name: "firework_rocket", Width: 0.25, Height: 0.25, Type: "mob", Category: Unknown}
Fox = Entity{ID: 28, InternalID: 28, DisplayName: "Fox", Name: "fox", Width: 0.6, Height: 0.7, Type: "mob", Category: Unknown}
Ghast = Entity{ID: 29, InternalID: 29, DisplayName: "Ghast", Name: "ghast", Width: 4, Height: 4, Type: "mob", Category: HostileMob}
Giant = Entity{ID: 30, InternalID: 30, DisplayName: "Giant", Name: "giant", Width: 3.6, Height: 12, Type: "mob", Category: HostileMob}
Guardian = Entity{ID: 31, InternalID: 31, DisplayName: "Guardian", Name: "guardian", Width: 0.85, Height: 0.85, Type: "mob", Category: HostileMob}
Hoglin = Entity{ID: 32, InternalID: 32, DisplayName: "Hoglin", Name: "hoglin", Width: 1.39648, Height: 1.4, Type: "UNKNOWN", Category: Unknown}
Horse = Entity{ID: 33, InternalID: 33, DisplayName: "Horse", Name: "horse", Width: 1.39648, Height: 1.6, Type: "mob", Category: PassiveMob}
Husk = Entity{ID: 34, InternalID: 34, DisplayName: "Husk", Name: "husk", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
Illusioner = Entity{ID: 35, InternalID: 35, DisplayName: "Illusioner", Name: "illusioner", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
IronGolem = Entity{ID: 36, InternalID: 36, DisplayName: "Iron Golem", Name: "iron_golem", Width: 1.4, Height: 2.7, Type: "mob", Category: PassiveMob}
Item = Entity{ID: 37, InternalID: 37, DisplayName: "Item", Name: "item", Width: 0.25, Height: 0.25, Type: "mob", Category: Drops}
ItemFrame = Entity{ID: 38, InternalID: 38, DisplayName: "Item Frame", Name: "item_frame", Width: 0.5, Height: 0.5, Type: "mob", Category: Immobile}
Fireball = Entity{ID: 39, InternalID: 39, DisplayName: "Fireball", Name: "fireball", Width: 1, Height: 1, Type: "mob", Category: Projectiles}
LeashKnot = Entity{ID: 40, InternalID: 40, DisplayName: "Leash Knot", Name: "leash_knot", Width: 0.5, Height: 0.5, Type: "mob", Category: Immobile}
LightningBolt = Entity{ID: 41, InternalID: 41, DisplayName: "Lightning Bolt", Name: "lightning_bolt", Width: 0, Height: 0, Type: "mob", Category: Unknown}
Llama = Entity{ID: 42, InternalID: 42, DisplayName: "Llama", Name: "llama", Width: 0.9, Height: 1.87, Type: "mob", Category: PassiveMob}
LlamaSpit = Entity{ID: 43, InternalID: 43, DisplayName: "Llama Spit", Name: "llama_spit", Width: 0.25, Height: 0.25, Type: "mob", Category: Projectiles}
MagmaCube = Entity{ID: 44, InternalID: 44, DisplayName: "Magma Cube", Name: "magma_cube", Width: 2.04, Height: 2.04, Type: "mob", Category: HostileMob}
Minecart = Entity{ID: 45, InternalID: 45, DisplayName: "Minecart", Name: "minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
ChestMinecart = Entity{ID: 46, InternalID: 46, DisplayName: "Minecart with Chest", Name: "chest_minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
CommandBlockMinecart = Entity{ID: 47, InternalID: 47, DisplayName: "Minecart with Command Block", Name: "command_block_minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
FurnaceMinecart = Entity{ID: 48, InternalID: 48, DisplayName: "Minecart with Furnace", Name: "furnace_minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
HopperMinecart = Entity{ID: 49, InternalID: 49, DisplayName: "Minecart with Hopper", Name: "hopper_minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
SpawnerMinecart = Entity{ID: 50, InternalID: 50, DisplayName: "Minecart with Spawner", Name: "spawner_minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
TntMinecart = Entity{ID: 51, InternalID: 51, DisplayName: "Minecart with TNT", Name: "tnt_minecart", Width: 0.98, Height: 0.7, Type: "mob", Category: Vehicles}
Mule = Entity{ID: 52, InternalID: 52, DisplayName: "Mule", Name: "mule", Width: 1.39648, Height: 1.6, Type: "mob", Category: PassiveMob}
Mooshroom = Entity{ID: 53, InternalID: 53, DisplayName: "Mooshroom", Name: "mooshroom", Width: 0.9, Height: 1.4, Type: "mob", Category: PassiveMob}
Ocelot = Entity{ID: 54, InternalID: 54, DisplayName: "Ocelot", Name: "ocelot", Width: 0.6, Height: 0.7, Type: "mob", Category: PassiveMob}
Painting = Entity{ID: 55, InternalID: 55, DisplayName: "Painting", Name: "painting", Width: 0.5, Height: 0.5, Type: "mob", Category: Immobile}
Panda = Entity{ID: 56, InternalID: 56, DisplayName: "Panda", Name: "panda", Width: 1.3, Height: 1.25, Type: "mob", Category: PassiveMob}
Parrot = Entity{ID: 57, InternalID: 57, DisplayName: "Parrot", Name: "parrot", Width: 0.5, Height: 0.9, Type: "mob", Category: PassiveMob}
Phantom = Entity{ID: 58, InternalID: 58, DisplayName: "Phantom", Name: "phantom", Width: 0.9, Height: 0.5, Type: "mob", Category: HostileMob}
Pig = Entity{ID: 59, InternalID: 59, DisplayName: "Pig", Name: "pig", Width: 0.9, Height: 0.9, Type: "mob", Category: PassiveMob}
Piglin = Entity{ID: 60, InternalID: 60, DisplayName: "Piglin", Name: "piglin", Width: 0.6, Height: 1.95, Type: "UNKNOWN", Category: Unknown}
PiglinBrute = Entity{ID: 61, InternalID: 61, DisplayName: "Piglin Brute", Name: "piglin_brute", Width: 0.6, Height: 1.95, Type: "UNKNOWN", Category: Unknown}
Pillager = Entity{ID: 62, InternalID: 62, DisplayName: "Pillager", Name: "pillager", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
PolarBear = Entity{ID: 63, InternalID: 63, DisplayName: "Polar Bear", Name: "polar_bear", Width: 1.4, Height: 1.4, Type: "mob", Category: PassiveMob}
Tnt = Entity{ID: 64, InternalID: 64, DisplayName: "Primed TNT", Name: "tnt", Width: 0.98, Height: 0.98, Type: "mob", Category: Blocks}
Pufferfish = Entity{ID: 65, InternalID: 65, DisplayName: "Pufferfish", Name: "pufferfish", Width: 0.7, Height: 0.7, Type: "mob", Category: PassiveMob}
Rabbit = Entity{ID: 66, InternalID: 66, DisplayName: "Rabbit", Name: "rabbit", Width: 0.4, Height: 0.5, Type: "mob", Category: PassiveMob}
Ravager = Entity{ID: 67, InternalID: 67, DisplayName: "Ravager", Name: "ravager", Width: 1.95, Height: 2.2, Type: "mob", Category: HostileMob}
Salmon = Entity{ID: 68, InternalID: 68, DisplayName: "Salmon", Name: "salmon", Width: 0.7, Height: 0.4, Type: "mob", Category: PassiveMob}
Sheep = Entity{ID: 69, InternalID: 69, DisplayName: "Sheep", Name: "sheep", Width: 0.9, Height: 1.3, Type: "mob", Category: PassiveMob}
Shulker = Entity{ID: 70, InternalID: 70, DisplayName: "Shulker", Name: "shulker", Width: 1, Height: 1, Type: "mob", Category: HostileMob}
ShulkerBullet = Entity{ID: 71, InternalID: 71, DisplayName: "Shulker Bullet", Name: "shulker_bullet", Width: 0.3125, Height: 0.3125, Type: "mob", Category: Projectiles}
Silverfish = Entity{ID: 72, InternalID: 72, DisplayName: "Silverfish", Name: "silverfish", Width: 0.4, Height: 0.3, Type: "mob", Category: HostileMob}
Skeleton = Entity{ID: 73, InternalID: 73, DisplayName: "Skeleton", Name: "skeleton", Width: 0.6, Height: 1.99, Type: "mob", Category: HostileMob}
SkeletonHorse = Entity{ID: 74, InternalID: 74, DisplayName: "Skeleton Horse", Name: "skeleton_horse", Width: 1.39648, Height: 1.6, Type: "mob", Category: PassiveMob}
Slime = Entity{ID: 75, InternalID: 75, DisplayName: "Slime", Name: "slime", Width: 2.04, Height: 2.04, Type: "mob", Category: HostileMob}
SmallFireball = Entity{ID: 76, InternalID: 76, DisplayName: "Small Fireball", Name: "small_fireball", Width: 0.3125, Height: 0.3125, Type: "mob", Category: Projectiles}
SnowGolem = Entity{ID: 77, InternalID: 77, DisplayName: "Snow Golem", Name: "snow_golem", Width: 0.7, Height: 1.9, Type: "mob", Category: PassiveMob}
Snowball = Entity{ID: 78, InternalID: 78, DisplayName: "Snowball", Name: "snowball", Width: 0.25, Height: 0.25, Type: "mob", Category: Projectiles}
SpectralArrow = Entity{ID: 79, InternalID: 79, DisplayName: "Spectral Arrow", Name: "spectral_arrow", Width: 0.5, Height: 0.5, Type: "mob", Category: Projectiles}
Spider = Entity{ID: 80, InternalID: 80, DisplayName: "Spider", Name: "spider", Width: 1.4, Height: 0.9, Type: "mob", Category: HostileMob}
Squid = Entity{ID: 81, InternalID: 81, DisplayName: "Squid", Name: "squid", Width: 0.8, Height: 0.8, Type: "mob", Category: PassiveMob}
Stray = Entity{ID: 82, InternalID: 82, DisplayName: "Stray", Name: "stray", Width: 0.6, Height: 1.99, Type: "mob", Category: HostileMob}
Strider = Entity{ID: 83, InternalID: 83, DisplayName: "Strider", Name: "strider", Width: 0.9, Height: 1.7, Type: "UNKNOWN", Category: Unknown}
Egg = Entity{ID: 84, InternalID: 84, DisplayName: "Thrown Egg", Name: "egg", Width: 0.25, Height: 0.25, Type: "mob", Category: Projectiles}
EnderPearl = Entity{ID: 85, InternalID: 85, DisplayName: "Thrown Ender Pearl", Name: "ender_pearl", Width: 0.25, Height: 0.25, Type: "mob", Category: Projectiles}
ExperienceBottle = Entity{ID: 86, InternalID: 86, DisplayName: "Thrown Bottle o' Enchanting", Name: "experience_bottle", Width: 0.25, Height: 0.25, Type: "mob", Category: Unknown}
Potion = Entity{ID: 87, InternalID: 87, DisplayName: "Potion", Name: "potion", Width: 0.25, Height: 0.25, Type: "mob", Category: Projectiles}
Trident = Entity{ID: 88, InternalID: 88, DisplayName: "Trident", Name: "trident", Width: 0.5, Height: 0.5, Type: "mob", Category: Unknown}
TraderLlama = Entity{ID: 89, InternalID: 89, DisplayName: "Trader Llama", Name: "trader_llama", Width: 0.9, Height: 1.87, Type: "mob", Category: PassiveMob}
TropicalFish = Entity{ID: 90, InternalID: 90, DisplayName: "Tropical Fish", Name: "tropical_fish", Width: 0.5, Height: 0.4, Type: "mob", Category: PassiveMob}
Turtle = Entity{ID: 91, InternalID: 91, DisplayName: "Turtle", Name: "turtle", Width: 1.2, Height: 0.4, Type: "mob", Category: PassiveMob}
Vex = Entity{ID: 92, InternalID: 92, DisplayName: "Vex", Name: "vex", Width: 0.4, Height: 0.8, Type: "mob", Category: HostileMob}
Villager = Entity{ID: 93, InternalID: 93, DisplayName: "Villager", Name: "villager", Width: 0.6, Height: 1.95, Type: "mob", Category: PassiveMob}
Vindicator = Entity{ID: 94, InternalID: 94, DisplayName: "Vindicator", Name: "vindicator", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
WanderingTrader = Entity{ID: 95, InternalID: 95, DisplayName: "Wandering Trader", Name: "wandering_trader", Width: 0.6, Height: 1.95, Type: "mob", Category: PassiveMob}
Witch = Entity{ID: 96, InternalID: 96, DisplayName: "Witch", Name: "witch", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
Wither = Entity{ID: 97, InternalID: 97, DisplayName: "Wither", Name: "wither", Width: 0.9, Height: 3.5, Type: "mob", Category: HostileMob}
WitherSkeleton = Entity{ID: 98, InternalID: 98, DisplayName: "Wither Skeleton", Name: "wither_skeleton", Width: 0.7, Height: 2.4, Type: "mob", Category: HostileMob}
WitherSkull = Entity{ID: 99, InternalID: 99, DisplayName: "Wither Skull", Name: "wither_skull", Width: 0.3125, Height: 0.3125, Type: "mob", Category: Projectiles}
Wolf = Entity{ID: 100, InternalID: 100, DisplayName: "Wolf", Name: "wolf", Width: 0.6, Height: 0.85, Type: "mob", Category: PassiveMob}
Zoglin = Entity{ID: 101, InternalID: 101, DisplayName: "Zoglin", Name: "zoglin", Width: 1.39648, Height: 1.4, Type: "UNKNOWN", Category: Unknown}
Zombie = Entity{ID: 102, InternalID: 102, DisplayName: "Zombie", Name: "zombie", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
ZombieHorse = Entity{ID: 103, InternalID: 103, DisplayName: "Zombie Horse", Name: "zombie_horse", Width: 1.39648, Height: 1.6, Type: "mob", Category: PassiveMob}
ZombieVillager = Entity{ID: 104, InternalID: 104, DisplayName: "Zombie Villager", Name: "zombie_villager", Width: 0.6, Height: 1.95, Type: "mob", Category: HostileMob}
ZombifiedPiglin = Entity{ID: 105, InternalID: 105, DisplayName: "Zombified Piglin", Name: "zombified_piglin", Width: 0.6, Height: 1.95, Type: "UNKNOWN", Category: Unknown}
Player = Entity{ID: 106, InternalID: 106, DisplayName: "Player", Name: "player", Width: 0.6, Height: 1.8, Type: "mob", Category: Unknown}
FishingBobber = Entity{ID: 107, InternalID: 107, DisplayName: "Fishing Bobber", Name: "fishing_bobber", Width: 0.25, Height: 0.25, Type: "mob", Category: Unknown}
)
// ByID is an index of minecraft entities by their ID.
var ByID = map[ID]*Entity{
0: &AreaEffectCloud,
1: &ArmorStand,
2: &Arrow,
3: &Bat,
4: &Bee,
5: &Blaze,
6: &Boat,
7: &Cat,
8: &CaveSpider,
9: &Chicken,
10: &Cod,
11: &Cow,
12: &Creeper,
13: &Dolphin,
14: &Donkey,
15: &DragonFireball,
16: &Drowned,
17: &ElderGuardian,
18: &EndCrystal,
19: &EnderDragon,
20: &Enderman,
21: &Endermite,
22: &Evoker,
23: &EvokerFangs,
24: &ExperienceOrb,
25: &EyeOfEnder,
26: &FallingBlock,
27: &FireworkRocket,
28: &Fox,
29: &Ghast,
30: &Giant,
31: &Guardian,
32: &Hoglin,
33: &Horse,
34: &Husk,
35: &Illusioner,
36: &IronGolem,
37: &Item,
38: &ItemFrame,
39: &Fireball,
40: &LeashKnot,
41: &LightningBolt,
42: &Llama,
43: &LlamaSpit,
44: &MagmaCube,
45: &Minecart,
46: &ChestMinecart,
47: &CommandBlockMinecart,
48: &FurnaceMinecart,
49: &HopperMinecart,
50: &SpawnerMinecart,
51: &TntMinecart,
52: &Mule,
53: &Mooshroom,
54: &Ocelot,
55: &Painting,
56: &Panda,
57: &Parrot,
58: &Phantom,
59: &Pig,
60: &Piglin,
61: &PiglinBrute,
62: &Pillager,
63: &PolarBear,
64: &Tnt,
65: &Pufferfish,
66: &Rabbit,
67: &Ravager,
68: &Salmon,
69: &Sheep,
70: &Shulker,
71: &ShulkerBullet,
72: &Silverfish,
73: &Skeleton,
74: &SkeletonHorse,
75: &Slime,
76: &SmallFireball,
77: &SnowGolem,
78: &Snowball,
79: &SpectralArrow,
80: &Spider,
81: &Squid,
82: &Stray,
83: &Strider,
84: &Egg,
85: &EnderPearl,
86: &ExperienceBottle,
87: &Potion,
88: &Trident,
89: &TraderLlama,
90: &TropicalFish,
91: &Turtle,
92: &Vex,
93: &Villager,
94: &Vindicator,
95: &WanderingTrader,
96: &Witch,
97: &Wither,
98: &WitherSkeleton,
99: &WitherSkull,
100: &Wolf,
101: &Zoglin,
102: &Zombie,
103: &ZombieHorse,
104: &ZombieVillager,
105: &ZombifiedPiglin,
106: &Player,
107: &FishingBobber,
}

184
data/entity/gen_entity.go Normal file
View File

@ -0,0 +1,184 @@
// gen_entity.go generates entity information.
//+build ignore
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/format"
"go/token"
"net/http"
"os"
"reflect"
"strconv"
"github.com/iancoleman/strcase"
)
const (
infoURL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.16.2/entities.json"
)
type Entity struct {
ID uint32 `json:"id"`
InternalID uint32 `json:"internalId"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Type string `json:"type"`
Category string `json:"category"`
}
func downloadInfo() ([]Entity, error) {
resp, err := http.Get(infoURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data []Entity
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return data, nil
}
func makeEntityDeclaration(entities []Entity) *ast.DeclStmt {
out := &ast.DeclStmt{Decl: &ast.GenDecl{Tok: token.VAR}}
for _, e := range entities {
t := reflect.TypeOf(e)
fields := make([]ast.Expr, t.NumField())
for i := 0; i < t.NumField(); i++ {
ft := t.Field(i)
if ft.Name == "Category" {
val := &ast.BasicLit{Kind: token.IDENT, Value: "Unknown"}
switch e.Category {
case "Passive mobs":
val.Value = "PassiveMob"
case "Hostile mobs":
val.Value = "HostileMob"
case "UNKNOWN":
default:
val.Value = e.Category
}
fields[i] = &ast.KeyValueExpr{
Key: &ast.Ident{Name: ft.Name},
Value: val,
}
continue
}
var val ast.Expr
switch ft.Type.Kind() {
case reflect.Uint32, reflect.Int:
val = &ast.BasicLit{Kind: token.INT, Value: fmt.Sprint(reflect.ValueOf(e).Field(i))}
case reflect.Float64:
val = &ast.BasicLit{Kind: token.FLOAT, Value: fmt.Sprint(reflect.ValueOf(e).Field(i))}
case reflect.String:
val = &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(reflect.ValueOf(e).Field(i).String())}
case reflect.Bool:
val = &ast.BasicLit{Kind: token.IDENT, Value: fmt.Sprint(reflect.ValueOf(e).Field(i).Bool())}
case reflect.Slice:
val = &ast.CompositeLit{
Type: &ast.ArrayType{
Elt: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Elem().Name()},
},
}
v := reflect.ValueOf(e).Field(i)
switch ft.Type.Elem().Kind() {
case reflect.Uint32, reflect.Int:
for x := 0; x < v.Len(); x++ {
val.(*ast.CompositeLit).Elts = append(val.(*ast.CompositeLit).Elts, &ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprint(v.Index(x)),
})
}
}
}
fields[i] = &ast.KeyValueExpr{
Key: &ast.Ident{Name: ft.Name},
Value: val,
}
}
out.Decl.(*ast.GenDecl).Specs = append(out.Decl.(*ast.GenDecl).Specs, &ast.ValueSpec{
Names: []*ast.Ident{{Name: strcase.ToCamel(e.Name)}},
Values: []ast.Expr{
&ast.CompositeLit{
Type: &ast.Ident{Name: reflect.TypeOf(e).Name()},
Elts: fields,
},
},
})
}
return out
}
func main() {
entities, err := downloadInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(`// Package entity stores information about entities in Minecraft.
package entity
// ID describes the numeric ID of an entity.
type ID uint32
// Category groups like entities.
type Category uint8
// Valid entity categories.
const (
Unknown Category = iota
Blocks
Immobile
Vehicles
Drops
Projectiles
PassiveMob
HostileMob
)
// Entity describes information about a type of entity.
type Entity struct {
ID ID
InternalID uint32
DisplayName string
Name string
Width float64
Height float64
Type string
Category Category
}
`)
format.Node(os.Stdout, token.NewFileSet(), makeEntityDeclaration(entities))
fmt.Println()
fmt.Println()
fmt.Println("// ByID is an index of minecraft entities by their ID.")
fmt.Println("var ByID = map[ID]*Entity{")
for _, e := range entities {
fmt.Printf(" %d: &%s,\n", e.ID, strcase.ToCamel(e.Name))
}
fmt.Println("}")
fmt.Println()
}

196
data/gen_packetIDs.go Normal file
View File

@ -0,0 +1,196 @@
// gen_packetIDs.go generates the enumeration of packet IDs used on the wire.
//+build ignore
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/iancoleman/strcase"
)
const (
protocolURL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.16.2/protocol.json"
)
// unnest is a utility function to unpack a value from a nested map, given
// an arbitrary set of keys to reach through.
func unnest(input map[string]interface{}, keys ...string) (map[string]interface{}, error) {
for _, k := range keys {
sub, ok := input[k]
if !ok {
return nil, fmt.Errorf("key %q not found", k)
}
next, ok := sub.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("key %q was %T, expected a string map", k, sub)
}
input = next
}
return input, nil
}
type duplexMappings struct {
Clientbound map[string]string
Serverbound map[string]string
}
func (m *duplexMappings) EnsureUniqueNames() {
// Assemble a slice of keys to check across both maps, because we cannot
// mutate a map while iterating it.
clientKeys := make([]string, 0, len(m.Clientbound))
for k, _ := range m.Clientbound {
clientKeys = append(clientKeys, k)
}
for _, k := range clientKeys {
if _, alsoServerKey := m.Serverbound[k]; alsoServerKey {
cVal, sVal := m.Clientbound[k], m.Serverbound[k]
delete(m.Clientbound, k)
delete(m.Serverbound, k)
m.Clientbound[k+"Clientbound"] = cVal
m.Serverbound[k+"Serverbound"] = sVal
}
}
}
// unpackMapping returns the set of packet IDs and their names for a given
// game state.
func unpackMapping(data map[string]interface{}, gameState string) (duplexMappings, error) {
out := duplexMappings{
Clientbound: make(map[string]string),
Serverbound: make(map[string]string),
}
info, err := unnest(data, gameState, "toClient", "types")
if err != nil {
return duplexMappings{}, err
}
pType := info["packet"].([]interface{})[1].([]interface{})[0].(map[string]interface{})["type"]
mappings := pType.([]interface{})[1].(map[string]interface{})["mappings"].(map[string]interface{})
for k, v := range mappings {
out.Clientbound[strcase.ToCamel(v.(string))] = k
}
info, err = unnest(data, gameState, "toServer", "types")
if err != nil {
return duplexMappings{}, err
}
pType = info["packet"].([]interface{})[1].([]interface{})[0].(map[string]interface{})["type"]
mappings = pType.([]interface{})[1].(map[string]interface{})["mappings"].(map[string]interface{})
for k, v := range mappings {
out.Serverbound[strcase.ToCamel(v.(string))] = k
}
return out, nil
}
type protocolIDs struct {
Login duplexMappings
Play duplexMappings
Status duplexMappings
// Handshake state has no packets
}
func (p protocolIDs) MaxLen() int {
var max int
for _, m := range []duplexMappings{p.Login, p.Play, p.Status} {
for k, _ := range m.Clientbound {
if len(k) > max {
max = len(k)
}
}
for k, _ := range m.Serverbound {
if len(k) > max {
max = len(k)
}
}
}
return max
}
func downloadInfo() (*protocolIDs, error) {
resp, err := http.Get(protocolURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
var out protocolIDs
if out.Login, err = unpackMapping(data, "login"); err != nil {
return nil, fmt.Errorf("login: %v", err)
}
out.Login.EnsureUniqueNames()
if out.Play, err = unpackMapping(data, "play"); err != nil {
return nil, fmt.Errorf("play: %v", err)
}
out.Play.EnsureUniqueNames()
if out.Status, err = unpackMapping(data, "status"); err != nil {
return nil, fmt.Errorf("play: %v", err)
}
out.Status.EnsureUniqueNames()
return &out, nil
}
func main() {
pIDs, err := downloadInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
maxLen := pIDs.MaxLen()
fmt.Println("package data")
fmt.Println()
fmt.Println("//go:generate bash -c \"go run gen_packetIDs.go > packetIDs.go\"")
fmt.Println()
fmt.Println("// This file is automatically generated by gen_packetIDs.go. DO NOT EDIT.")
fmt.Println()
fmt.Println("// PktID represents a packet ID used in the minecraft protocol.")
fmt.Println("type PktID int32")
fmt.Println()
fmt.Println("// Valid PktID values.")
fmt.Println("const (")
fmt.Println(" // Clientbound packets for connections in the login state.")
for k, v := range pIDs.Login.Clientbound {
fmt.Printf(" %s%s PktID = %s\n", k, strings.Repeat(" ", maxLen-len(k)), v)
}
fmt.Println(" // Serverbound packets for connections in the login state.")
for k, v := range pIDs.Login.Serverbound {
fmt.Printf(" %s%s PktID = %s\n", k, strings.Repeat(" ", maxLen-len(k)), v)
}
fmt.Println()
fmt.Println(" // Clientbound packets for connections in the play state.")
for k, v := range pIDs.Play.Clientbound {
fmt.Printf(" %s%s PktID = %s\n", k, strings.Repeat(" ", maxLen-len(k)), v)
}
fmt.Println(" // Serverbound packets for connections in the play state.")
for k, v := range pIDs.Play.Serverbound {
fmt.Printf(" %s%s PktID = %s\n", k, strings.Repeat(" ", maxLen-len(k)), v)
}
fmt.Println()
fmt.Println(" // Clientbound packets used to respond to ping/status requests.")
for k, v := range pIDs.Status.Clientbound {
fmt.Printf(" %s%s PktID = %s\n", k, strings.Repeat(" ", maxLen-len(k)), v)
}
fmt.Println(" // Serverbound packets used to ping or read server status.")
for k, v := range pIDs.Status.Serverbound {
fmt.Printf(" %s%s PktID = %s\n", k, strings.Repeat(" ", maxLen-len(k)), v)
}
fmt.Println()
fmt.Println(")")
}

30
data/inv/inv.go Normal file
View File

@ -0,0 +1,30 @@
// Package inv maps window types to inventory slot information.
package inv
type Info struct {
Name string
Start, End int // Player inventory
Slots int
}
func (i Info) PlayerInvStart() int {
return i.Start
}
func (i Info) PlayerInvEnd() int {
return i.End
}
func (i Info) HotbarIdx(place int) int {
return i.End - (8 - place)
}
var ByType = map[int]Info{
-1: Info{Name: "inventory", Start: 9, End: 44, Slots: 46},
0: Info{Name: "generic_9x1", Start: 1 * 9, End: 1*9 + 35, Slots: 1*9 + 36},
1: Info{Name: "generic_9x2", Start: 2 * 9, End: 2*9 + 35, Slots: 2*9 + 36},
2: Info{Name: "generic_9x3", Start: 3 * 9, End: 3*9 + 35, Slots: 3*9 + 36},
3: Info{Name: "generic_9x4", Start: 4 * 9, End: 4*9 + 35, Slots: 4*9 + 36},
4: Info{Name: "generic_9x5", Start: 5 * 9, End: 5*9 + 35, Slots: 5*9 + 36},
5: Info{Name: "generic_9x6", Start: 6 * 9, End: 6*9 + 35, Slots: 6*9 + 36},
}

168
data/item/gen_item.go Normal file
View File

@ -0,0 +1,168 @@
// gen_blocks.go generates block information.
//+build ignore
package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/format"
"go/token"
"net/http"
"os"
"reflect"
"strconv"
"github.com/iancoleman/strcase"
)
const (
infoURL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.16.2/items.json"
)
type Item struct {
ID uint32 `json:"id"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
StackSize uint `json:"stackSize"`
}
func downloadInfo() ([]Item, error) {
resp, err := http.Get(infoURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data []Item
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
return data, nil
}
func makeItemDeclaration(blocks []Item) *ast.DeclStmt {
out := &ast.DeclStmt{Decl: &ast.GenDecl{Tok: token.VAR}}
for _, b := range blocks {
t := reflect.TypeOf(b)
fields := make([]ast.Expr, t.NumField())
for i := 0; i < t.NumField(); i++ {
ft := t.Field(i)
var val ast.Expr
switch ft.Type.Kind() {
case reflect.Uint32, reflect.Int, reflect.Uint:
val = &ast.BasicLit{Kind: token.INT, Value: fmt.Sprint(reflect.ValueOf(b).Field(i))}
case reflect.Float64:
val = &ast.BasicLit{Kind: token.FLOAT, Value: fmt.Sprint(reflect.ValueOf(b).Field(i))}
case reflect.String:
val = &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(reflect.ValueOf(b).Field(i).String())}
case reflect.Bool:
val = &ast.BasicLit{Kind: token.IDENT, Value: fmt.Sprint(reflect.ValueOf(b).Field(i).Bool())}
case reflect.Slice:
val = &ast.CompositeLit{
Type: &ast.ArrayType{
Elt: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Elem().Name()},
},
}
v := reflect.ValueOf(b).Field(i)
switch ft.Type.Elem().Kind() {
case reflect.Uint32, reflect.Int:
for x := 0; x < v.Len(); x++ {
val.(*ast.CompositeLit).Elts = append(val.(*ast.CompositeLit).Elts, &ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprint(v.Index(x)),
})
}
}
case reflect.Map:
// Must be the NeedsTools map of type map[uint32]bool.
m := &ast.CompositeLit{
Type: &ast.MapType{
Key: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Key().Name()},
Value: &ast.BasicLit{Kind: token.IDENT, Value: ft.Type.Elem().Name()},
},
}
iter := reflect.ValueOf(b).Field(i).MapRange()
for iter.Next() {
m.Elts = append(m.Elts, &ast.KeyValueExpr{
Key: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprint(iter.Key().Uint())},
Value: &ast.BasicLit{Kind: token.IDENT, Value: fmt.Sprint(iter.Value().Bool())},
})
}
val = m
}
fields[i] = &ast.KeyValueExpr{
Key: &ast.Ident{Name: ft.Name},
Value: val,
}
}
out.Decl.(*ast.GenDecl).Specs = append(out.Decl.(*ast.GenDecl).Specs, &ast.ValueSpec{
Names: []*ast.Ident{{Name: strcase.ToCamel(b.Name)}},
Values: []ast.Expr{
&ast.CompositeLit{
Type: &ast.Ident{Name: reflect.TypeOf(b).Name()},
Elts: fields,
},
},
})
}
return out
}
func main() {
items, err := downloadInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(`// Package item stores information about items in Minecraft.
package item
import (
"math"
)
// ID describes the numeric ID of an item.
type ID uint32
// Item describes information about a type of item.
type Item struct {
ID ID
DisplayName string
Name string
StackSize uint
}
`)
format.Node(os.Stdout, token.NewFileSet(), makeItemDeclaration(items))
fmt.Println()
fmt.Println()
fmt.Println("// ByID is an index of minecraft items by their ID.")
fmt.Println("var ByID = map[ID]*Item{")
for _, i := range items {
fmt.Printf(" %d: &%s,\n", i.ID, strcase.ToCamel(i.Name))
}
fmt.Println("}")
fmt.Println()
fmt.Println("// ByName is an index of minecraft items by their name.")
fmt.Println("var ByName = map[string]*Item{")
for _, i := range items {
fmt.Printf(" %q: &%s,\n", i.Name, strcase.ToCamel(i.Name))
}
fmt.Println("}")
fmt.Println()
}

2952
data/item/item.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,156 +1,173 @@
package data package data
// Clientbound packet IDs //go:generate bash -c "go run gen_packetIDs.go > packetIDs.go"
// This file is automatically generated by gen_packetIDs.go. DO NOT EDIT.
// PktID represents a packet ID used in the minecraft protocol.
type PktID int32
// Valid PktID values.
const ( const (
SpawnObject int32 = iota //0x00 // Clientbound packets for connections in the login state.
SpawnExperienceOrb EncryptionBeginClientbound PktID = 0x01
SpawnLivingEntity Success PktID = 0x02
SpawnPainting Compress PktID = 0x03
SpawnPlayer LoginPluginRequest PktID = 0x04
EntityAnimationClientbound Disconnect PktID = 0x00
Statistics // Serverbound packets for connections in the login state.
AcknowledgePlayerDigging EncryptionBeginServerbound PktID = 0x01
BlockBreakAnimation LoginPluginResponse PktID = 0x02
BlockEntityData LoginStart PktID = 0x00
BlockAction
BlockChange
BossBar
ServerDifficulty
ChatMessageClientbound
TabComplete
DeclareCommands //0x10 // Clientbound packets for connections in the play state.
WindowConfirmationClientbound EntityMetadata PktID = 0x44
CloseWindowClientbound Teams PktID = 0x4c
WindowItems BossBar PktID = 0x0c
WindowProperty Map PktID = 0x25
SetSlot Difficulty PktID = 0x0d
SetCooldown Camera PktID = 0x3e
PluginMessageClientbound WindowItems PktID = 0x13
NamedSoundEffect ScoreboardObjective PktID = 0x4a
DisconnectPlay RelEntityMove PktID = 0x27
EntityStatus DeclareCommands PktID = 0x10
Explosion CombatEvent PktID = 0x31
UnloadChunk SpawnEntityPainting PktID = 0x03
ChangeGameState EntityMoveLook PktID = 0x28
OpenHorseWindow ScoreboardScore PktID = 0x4d
KeepAliveClientbound Title PktID = 0x4f
CraftProgressBar PktID = 0x14
NamedEntitySpawn PktID = 0x04
ScoreboardDisplayObjective PktID = 0x43
WorldParticles PktID = 0x22
OpenWindow PktID = 0x2d
MultiBlockChange PktID = 0x3b
EntitySoundEffect PktID = 0x50
Tags PktID = 0x5b
EntityUpdateAttributes PktID = 0x58
NamedSoundEffect PktID = 0x18
GameStateChange PktID = 0x1d
PlayerInfo PktID = 0x32
Advancements PktID = 0x57
Explosion PktID = 0x1b
KeepAliveClientbound PktID = 0x1f
MapChunk PktID = 0x20
AttachEntity PktID = 0x45
TradeList PktID = 0x26
Respawn PktID = 0x39
EntityDestroy PktID = 0x36
Experience PktID = 0x48
EntityLook PktID = 0x29
OpenBook PktID = 0x2c
WorldEvent PktID = 0x21
DeclareRecipes PktID = 0x5a
UnlockRecipes PktID = 0x35
EntityEquipment PktID = 0x47
EntityVelocity PktID = 0x46
Animation PktID = 0x05
UpdateViewDistance PktID = 0x41
HeldItemSlotClientbound PktID = 0x3f
NbtQueryResponse PktID = 0x54
Entity PktID = 0x2a
UpdateViewPosition PktID = 0x40
AbilitiesClientbound PktID = 0x30
OpenSignEntity PktID = 0x2e
SetSlot PktID = 0x15
PlayerlistHeader PktID = 0x53
ResourcePackSend PktID = 0x38
SpawnEntityExperienceOrb PktID = 0x01
Collect PktID = 0x55
Statistics PktID = 0x06
TileEntityData PktID = 0x09
ChatClientbound PktID = 0x0e
WorldBorder PktID = 0x3d
UnloadChunk PktID = 0x1c
UpdateLight PktID = 0x23
UpdateHealth PktID = 0x49
RemoveEntityEffect PktID = 0x37
KickDisconnect PktID = 0x19
CustomPayloadClientbound PktID = 0x17
SpawnPosition PktID = 0x42
EntityStatus PktID = 0x1a
SpawnEntity PktID = 0x00
SetPassengers PktID = 0x4b
FacePlayer PktID = 0x33
TransactionClientbound PktID = 0x11
BlockAction PktID = 0x0a
BlockBreakAnimation PktID = 0x08
BlockChange PktID = 0x0b
Login PktID = 0x24
VehicleMoveClientbound PktID = 0x2b
CraftRecipeResponse PktID = 0x2f
SetCooldown PktID = 0x16
StopSound PktID = 0x52
EntityEffect PktID = 0x59
CloseWindowClientbound PktID = 0x12
AcknowledgePlayerDigging PktID = 0x07
SelectAdvancementTab PktID = 0x3c
EntityHeadRotation PktID = 0x3a
TabCompleteClientbound PktID = 0x0f
PositionClientbound PktID = 0x34
OpenHorseWindow PktID = 0x1e
UpdateTime PktID = 0x4e
SpawnEntityLiving PktID = 0x02
EntityTeleport PktID = 0x56
SoundEffect PktID = 0x51
// Serverbound packets for connections in the play state.
SetBeaconEffect PktID = 0x24
UpdateStructureBlock PktID = 0x2a
EnchantItem PktID = 0x08
Spectate PktID = 0x2d
ArmAnimation PktID = 0x2c
QueryEntityNbt PktID = 0x0d
Flying PktID = 0x15
ResourcePackReceive PktID = 0x21
BlockDig PktID = 0x1b
AbilitiesServerbound PktID = 0x1a
SelectTrade PktID = 0x23
UpdateJigsawBlock PktID = 0x29
SteerVehicle PktID = 0x1d
Settings PktID = 0x05
SteerBoat PktID = 0x17
UseItem PktID = 0x2f
TeleportConfirm PktID = 0x00
PickItem PktID = 0x18
CraftRecipeRequest PktID = 0x19
Look PktID = 0x14
TabCompleteServerbound PktID = 0x06
AdvancementTab PktID = 0x22
ClientCommand PktID = 0x04
EntityAction PktID = 0x1c
PositionServerbound PktID = 0x12
TransactionServerbound PktID = 0x07
UpdateSign PktID = 0x2b
QueryBlockNbt PktID = 0x01
PositionLook PktID = 0x13
HeldItemSlotServerbound PktID = 0x25
EditBook PktID = 0x0c
UpdateCommandBlock PktID = 0x26
UseEntity PktID = 0x0e
GenerateStructure PktID = 0x0f
NameItem PktID = 0x20
RecipeBook PktID = 0x1f
KeepAliveServerbound PktID = 0x10
CustomPayloadServerbound PktID = 0x0b
VehicleMoveServerbound PktID = 0x16
CloseWindowServerbound PktID = 0x0a
UpdateCommandBlockMinecart PktID = 0x27
WindowClick PktID = 0x09
ChatServerbound PktID = 0x03
SetCreativeSlot PktID = 0x28
DisplayedRecipe PktID = 0x1e
BlockPlace PktID = 0x2e
LockDifficulty PktID = 0x11
SetDifficulty PktID = 0x02
ChunkData //0x20 // Clientbound packets used to respond to ping/status requests.
Effect ServerInfo PktID = 0x00
Particle PingClientbound PktID = 0x01
UpdateLight // Serverbound packets used to ping or read server status.
JoinGame PingStart PktID = 0x00
MapData PingServerbound PktID = 0x01
TradeList
EntityRelativeMove
EntityLookAndRelativeMove
EntityLook
Entity
VehicleMoveClientbound
OpenBook
OpenWindow
OpenSignEditor
CraftRecipeResponse
PlayerAbilitiesClientbound //0x30
CombatEvent
PlayerInfo
FacePlayer
PlayerPositionAndLookClientbound
UnlockRecipes
DestroyEntities
RemoveEntityEffect
ResourcePackSend
Respawn
EntityHeadLook
MultiBlockChange
SelectAdvancementTab
WorldBorder
Camera
HeldItemChangeClientbound
UpdateViewPosition //0x40
UpdateViewDistance
SpawnPosition
DisplayScoreboard
EntityMetadata
AttachEntity
EntityVelocity
EntityEquipment
SetExperience
UpdateHealth
ScoreboardObjective
SetPassengers
Teams
UpdateScore
TimeUpdate
Title
EntitySoundEffect //0x50
SoundEffect
StopSound
PlayerListHeaderAndFooter
NBTQueryResponse
CollectItem
EntityTeleport
Advancements
EntityProperties
EntityEffect
DeclareRecipes
Tags //0x5B
)
// Serverbound packet IDs
const (
TeleportConfirm int32 = iota //0x00
QueryBlockNBT
SetDifficulty
ChatMessageServerbound
ClientStatus
ClientSettings
TabCompleteServerbound
ConfirmTransactionServerbound
ClickWindowButton
ClickWindow
CloseWindowServerbound
PluginMessageServerbound
EditBook
QueryEntityNBT
UseEntity
GenerateStructure
KeepAliveServerbound //0x10
LockDifficulty
PlayerPosition
PlayerPositionAndLookServerbound
PlayerLook
Player
VehicleMoveServerbound
SteerBoat
PickItem
CraftRecipeRequest
PlayerAbilitiesServerbound
PlayerDigging
EntityAction
SteerVehicle
DisplayedRecipe
RecipeBookData
NameItem //0x20
ResourcePackStatus
AdvancementTab
SelectTrade
SetBeaconEffect
HeldItemChangeServerbound
UpdateCommandBlock
UpdateCommandBlockMinecart
CreativeInventoryAction
UpdateJigsawBlock
UpdateStructureBlock
UpdateSign
AnimationServerbound
Spectate
PlayerBlockPlacement
UseItem //0x2F
) )

6
go.mod
View File

@ -2,4 +2,8 @@ module github.com/Tnze/go-mc
go 1.13 go 1.13
require github.com/google/uuid v1.1.1 require (
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482
github.com/google/uuid v1.1.1
github.com/iancoleman/strcase v0.1.1 // indirect
)

4
go.sum
View File

@ -1,2 +1,6 @@
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 h1:p4g4uok3+r6Tg6fxXEQUAcMAX/WdK6WhkQW9s0jaT7k=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/iancoleman/strcase v0.1.1 h1:2I+LRClyCYB7JgZb9U0k75VHUiQe9RfknRqDyUfzp7k=
github.com/iancoleman/strcase v0.1.1/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=

View File

@ -5,6 +5,8 @@ import (
"compress/zlib" "compress/zlib"
"fmt" "fmt"
"io" "io"
"github.com/Tnze/go-mc/data"
) )
// Packet define a net data package // Packet define a net data package
@ -14,8 +16,8 @@ type Packet struct {
} }
//Marshal generate Packet with the ID and Fields //Marshal generate Packet with the ID and Fields
func Marshal(ID int32, fields ...FieldEncoder) (pk Packet) { func Marshal(id data.PktID, fields ...FieldEncoder) (pk Packet) {
pk.ID = ID pk.ID = int32(id)
for _, v := range fields { for _, v := range fields {
pk.Data = append(pk.Data, v.Encode()...) pk.Data = append(pk.Data, v.Encode()...)

View File

@ -2,10 +2,11 @@ package packet
import ( import (
"errors" "errors"
"github.com/google/uuid"
"io" "io"
"math" "math"
"github.com/google/uuid"
"github.com/Tnze/go-mc/nbt" "github.com/Tnze/go-mc/nbt"
) )
@ -364,6 +365,16 @@ func (p *Position) Decode(r DecodeReader) error {
return nil return nil
} }
//Decodes an Angle
func (b *Angle) Decode(r DecodeReader) error {
v, err := r.ReadByte()
if err != nil {
return err
}
*b = Angle(v)
return nil
}
//Encode a Float //Encode a Float
func (f Float) Encode() []byte { func (f Float) Encode() []byte {
return Int(math.Float32bits(float32(f))).Encode() return Int(math.Float32bits(float32(f))).Encode()

128
net/ptypes/chunk.go Normal file
View File

@ -0,0 +1,128 @@
// Package ptypes implements encoding and decoding for high-level packets.
package ptypes
import (
"bytes"
"fmt"
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/nbt"
pk "github.com/Tnze/go-mc/net/packet"
)
// ChunkData is a clientbound packet which describes a chunk.
type ChunkData struct {
X, Z pk.Int
FullChunk pk.Boolean
PrimaryBitMask pk.VarInt
Heightmaps struct{}
Biomes biomesData
Data chunkData
BlockEntities blockEntities
}
func (p *ChunkData) Decode(pkt pk.Packet) error {
r := bytes.NewReader(pkt.Data)
if err := p.X.Decode(r); err != nil {
return fmt.Errorf("X: %v", err)
}
if err := p.Z.Decode(r); err != nil {
return fmt.Errorf("Z: %v", err)
}
if err := p.FullChunk.Decode(r); err != nil {
return fmt.Errorf("full chunk: %v", err)
}
if err := p.PrimaryBitMask.Decode(r); err != nil {
return fmt.Errorf("bit mask: %v", err)
}
if err := (pk.NBT{V: &p.Heightmaps}).Decode(r); err != nil {
return fmt.Errorf("heightmaps: %v", err)
}
// Biome data is only present for full chunks.
if p.FullChunk {
if err := p.Biomes.Decode(r); err != nil {
return fmt.Errorf("heightmaps: %v", err)
}
}
if err := p.Data.Decode(r); err != nil {
return fmt.Errorf("data: %v", err)
}
if err := p.BlockEntities.Decode(r); err != nil {
return fmt.Errorf("block entities: %v", err)
}
return nil
}
type biomesData struct {
data []pk.VarInt
}
func (b *biomesData) Decode(r pk.DecodeReader) error {
var nobd pk.VarInt // Number of Biome Datums
if err := nobd.Decode(r); err != nil {
return err
}
b.data = make([]pk.VarInt, nobd)
for i := 0; i < int(nobd); i++ {
var d pk.VarInt
if err := d.Decode(r); err != nil {
return err
}
b.data[i] = d
}
return nil
}
type chunkData []byte
type blockEntities []entity.BlockEntity
// Decode implement net.packet.FieldDecoder
func (c *chunkData) Decode(r pk.DecodeReader) error {
var sz pk.VarInt
if err := sz.Decode(r); err != nil {
return err
}
*c = make([]byte, sz)
if _, err := r.Read(*c); err != nil {
return err
}
return nil
}
// Decode implement net.packet.FieldDecoder
func (b *blockEntities) Decode(r pk.DecodeReader) error {
var sz pk.VarInt // Number of BlockEntities
if err := sz.Decode(r); err != nil {
return err
}
*b = make(blockEntities, sz)
decoder := nbt.NewDecoder(r)
for i := 0; i < int(sz); i++ {
if err := decoder.Decode(&(*b)[i]); err != nil {
return err
}
}
return nil
}
// TileEntityData describes a change to a tile entity.
type TileEntityData struct {
Pos pk.Position
Action pk.UnsignedByte
Data entity.BlockEntity
}
func (p *TileEntityData) Decode(pkt pk.Packet) error {
r := bytes.NewReader(pkt.Data)
if err := p.Pos.Decode(r); err != nil {
return fmt.Errorf("position: %v", err)
}
if err := p.Action.Decode(r); err != nil {
return fmt.Errorf("action: %v", err)
}
return nbt.NewDecoder(r).Decode(&p.Data)
}

95
net/ptypes/entities.go Normal file
View File

@ -0,0 +1,95 @@
package ptypes
import pk "github.com/Tnze/go-mc/net/packet"
// SpawnEntity is a clientbound packet used to spawn a non-mob entity.
type SpawnEntity struct {
ID pk.VarInt
UUID pk.UUID
Type pk.VarInt
X, Y, Z pk.Double
Pitch, Yaw pk.Angle
Data pk.Int
VelX, VelY, VelZ pk.Short
}
func (p *SpawnEntity) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.UUID, &p.Type,
&p.X, &p.Y, &p.Z, &p.Pitch, &p.Yaw,
&p.Data, &p.VelX, &p.VelY, &p.VelZ)
}
// SpawnPlayer is a clientbound packet used to describe a player entering
// visible range.
type SpawnPlayer struct {
ID pk.VarInt
UUID pk.UUID
X, Y, Z pk.Double
Yaw, Pitch pk.Angle
}
func (p *SpawnPlayer) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.UUID, &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch)
}
// SpawnLivingEntity is a clientbound packet used to spawn a mob.
type SpawnLivingEntity struct {
ID pk.VarInt
UUID pk.UUID
Type pk.VarInt
X, Y, Z pk.Double
Yaw, Pitch pk.Angle
HeadPitch pk.Angle
VelX, VelY, VelZ pk.Short
}
func (p *SpawnLivingEntity) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.UUID, &p.Type,
&p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch,
&p.HeadPitch, &p.VelX, &p.VelY, &p.VelZ)
}
// EntityAnimationClientbound updates the animationf state of an entity.
type EntityAnimationClientbound struct {
ID pk.VarInt
Animation pk.UnsignedByte
}
func (p *EntityAnimationClientbound) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.Animation)
}
// EntityPosition is a clientbound packet used to update an entities position.
type EntityPosition struct {
ID pk.VarInt
X, Y, Z pk.Short // Deltas
OnGround pk.Boolean
}
func (p *EntityPosition) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.X, &p.Y, &p.Z, &p.OnGround)
}
// EntityPosition is a clientbound packet used to update an entities position
// and its rotation.
type EntityPositionLook struct {
ID pk.VarInt
X, Y, Z pk.Short // Deltas
Yaw, Pitch pk.Angle
OnGround pk.Boolean
}
func (p *EntityPositionLook) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.X, &p.Y, &p.Z, &p.Yaw, &p.Pitch, &p.OnGround)
}
// EntityRotation is a clientbound packet used to update an entities rotation.
type EntityRotation struct {
ID pk.VarInt
Yaw, Pitch pk.Angle
OnGround pk.Boolean
}
func (p *EntityRotation) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.ID, &p.Yaw, &p.Pitch, &p.OnGround)
}

89
net/ptypes/inventory.go Normal file
View File

@ -0,0 +1,89 @@
// Package ptypes implements encoding and decoding for high-level packets.
package ptypes
import (
"bytes"
"errors"
"github.com/Tnze/go-mc/bot/world/entity"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data"
"github.com/Tnze/go-mc/nbt"
pk "github.com/Tnze/go-mc/net/packet"
)
// SetSlot is a clientbound packet which configures an inventory slot.
// A window ID of -1 represents the cursor, and a window ID of 0 represents
// the players inventory.
type SetSlot struct {
WindowID pk.Byte
Slot pk.Short
SlotData entity.Slot
}
func (p *SetSlot) Decode(pkt pk.Packet) error {
if err := pkt.Scan(&p.WindowID, &p.Slot, &p.SlotData); err != nil && !errors.Is(err, nbt.ErrEND) {
return err
}
return nil
}
// WindowItems is a clientbound packet describing the contents of multiple
// inventory slots in a window/inventory.
type WindowItems struct {
WindowID pk.Byte
Slots []entity.Slot
}
func (p *WindowItems) Decode(pkt pk.Packet) error {
r := bytes.NewReader(pkt.Data)
if err := p.WindowID.Decode(r); err != nil {
return err
}
var count pk.Short
if err := count.Decode(r); err != nil {
return err
}
p.Slots = make([]entity.Slot, int(count))
for i := 0; i < int(count); i++ {
if err := p.Slots[i].Decode(r); err != nil && !errors.Is(err, nbt.ErrEND) {
return err
}
}
return nil
}
// OpenWindow is a clientbound packet which opens an inventory.
type OpenWindow struct {
WindowID pk.VarInt
WindowType pk.VarInt
Title chat.Message
}
func (p *OpenWindow) Decode(pkt pk.Packet) error {
if err := pkt.Scan(&p.WindowID, &p.WindowType, &p.Title); err != nil && !errors.Is(err, nbt.ErrEND) {
return err
}
return nil
}
type ConfirmTransaction struct {
WindowID pk.Byte
ActionID pk.Short
Accepted pk.Boolean
}
func (p *ConfirmTransaction) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.WindowID, &p.ActionID, &p.Accepted)
}
func (p ConfirmTransaction) Encode() pk.Packet {
return pk.Marshal(
data.TransactionServerbound,
p.WindowID,
p.ActionID,
p.Accepted,
)
}

91
net/ptypes/misc.go Normal file
View File

@ -0,0 +1,91 @@
package ptypes
import (
"io/ioutil"
"github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data"
pk "github.com/Tnze/go-mc/net/packet"
)
// SoundEffect is a clientbound packet used to play a specific sound ID
// on the client.
type SoundEffect struct {
Sound pk.VarInt
Category pk.VarInt
X, Y, Z pk.Int
Volume, Pitch pk.Float
}
func (p *SoundEffect) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Sound, &p.Category, &p.X, &p.Y, &p.Z, &p.Volume, &p.Pitch)
}
// NamedSoundEffect is a clientbound packet used to play a sound with the
// specified name on the client.
type NamedSoundEffect struct {
Sound pk.String
Category pk.VarInt
X, Y, Z pk.Int
Volume, Pitch pk.Float
}
func (p *NamedSoundEffect) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Sound, &p.Category, &p.X, &p.Y, &p.Z, &p.Volume, &p.Pitch)
}
// ChatMessageClientbound represents a chat message forwarded by the server.
type ChatMessageClientbound struct {
S chat.Message
Pos pk.Byte
Sender pk.UUID
}
func (p *ChatMessageClientbound) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.S, &p.Pos, &p.Sender)
}
// UpdateHealth encodes player health/food information from the server.
type UpdateHealth struct {
Health pk.Float
Food pk.VarInt
FoodSaturation pk.Float
}
func (p *UpdateHealth) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Health, &p.Food, &p.FoodSaturation)
}
// PluginData encodes the custom data encoded in a plugin message.
type PluginData []byte
func (p PluginData) Encode() []byte {
return []byte(p)
}
func (p *PluginData) Decode(r pk.DecodeReader) error {
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
*p = data
return nil
}
// PluginMessage represents a packet with a customized payload.
type PluginMessage struct {
Channel pk.Identifier
Data PluginData
}
func (p *PluginMessage) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.Channel, &p.Data)
}
func (p *PluginMessage) Encode() pk.Packet {
return pk.Marshal(
data.CustomPayloadServerbound,
p.Channel,
p.Data,
)
}

87
net/ptypes/motion.go Normal file
View File

@ -0,0 +1,87 @@
// Package ptypes implements encoding and decoding for high-level packets.
package ptypes
import (
"github.com/Tnze/go-mc/data"
pk "github.com/Tnze/go-mc/net/packet"
)
// PositionAndLookClientbound describes the location and orientation of
// the player.
type PositionAndLookClientbound struct {
X, Y, Z pk.Double
Yaw, Pitch pk.Float
Flags pk.Byte
TeleportID pk.VarInt
}
func (p *PositionAndLookClientbound) RelativeX() bool {
return p.Flags&0x01 != 0
}
func (p *PositionAndLookClientbound) RelativeY() bool {
return p.Flags&0x02 != 0
}
func (p *PositionAndLookClientbound) RelativeZ() bool {
return p.Flags&0x04 != 0
}
func (p *PositionAndLookClientbound) RelativeYaw() bool {
return p.Flags&0x08 != 0
}
func (p *PositionAndLookClientbound) RelativePitch() bool {
return p.Flags&0x10 != 0
}
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.Look,
pk.Float(p.Yaw),
pk.Float(p.Pitch),
pk.Boolean(p.OnGround),
)
}

32
net/ptypes/world.go Normal file
View File

@ -0,0 +1,32 @@
package ptypes
import (
pk "github.com/Tnze/go-mc/net/packet"
)
// JoinGame encodes global/world information from the server.
type JoinGame struct {
PlayerEntity pk.Int
Hardcore pk.Boolean
Gamemode pk.UnsignedByte
PrevGamemode pk.UnsignedByte
WorldCount pk.VarInt
WorldNames pk.Identifier
//DimensionCodec pk.NBT
Dimension pk.Int
WorldName pk.Identifier
HashedSeed pk.Long
maxPlayers pk.VarInt // Now ignored
ViewDistance pk.VarInt
RDI pk.Boolean // Reduced Debug Info
ERS pk.Boolean // Enable respawn screen
IsDebug pk.Boolean
IsFlat pk.Boolean
}
func (p *JoinGame) Decode(pkt pk.Packet) error {
return pkt.Scan(&p.PlayerEntity, &p.Hardcore, &p.Gamemode, &p.PrevGamemode,
&p.WorldCount, &p.WorldNames, &p.Dimension,
&p.WorldName, &p.HashedSeed, &p.maxPlayers, &p.ViewDistance,
&p.RDI, &p.ERS, &p.IsDebug, &p.IsFlat)
}