简介 · Introduction
Go-MC 是一个用 Go 语言编写的用于与 Minecraft 交互的库。最初从网络库开始,用纯 Go 实现了 MC 基于 TCP 的网络通信协议。
并提供了一个 bot
包以方便编写命令行MC机器人程序。
Go-MC 也提供了用于编写服务端的框架 server
,并且有一个独立的仓库作为一个服务端实现。服务端的开发暂不包含在本教程中。
在使用 bot
包的过程中,随着发送消息、玩家移动、解析区块等等新功能实现的需要,
逐步对游戏使用的基础数据结构如 基于JSON的聊天消息, NBT 二进制命名标签, 区域储存格式 .mca 文件 等编写了 Go 版本的实现。
- 在
net
包实现MC的网络传输协议及VarInt
等数据结构。 - 在
chat
包实现基于 JSON 的聊天消息格式的编解码。 - 在
level
包实现表示区块的数据结构、BitStorage、调色板等等。 - 在
level/block
包提供了方块表示数据结构及方块状态表。 - 在
nbt
包实现基于反射的二进制命名标签格式编解码,提供与标准库json
类似的API。 - 在
offline
包提供离线模式下将玩家名转换为UUID的算法。 - 在
save/region
包中实现了.mca
文件的读取和写入。 - 而
yggdrasil
和realms
包已经不再维护了。
在添加这些功能时,都同时提供了客户端和服务端侧的接口,例如我们的 RCON 库就提供了服务端的实现。
所以 Go-MC 不仅能用来编写客户端,也可以用来实现服务端。在 server
包中还提供了相应的框架。
翻译 · Translation
本文本应使用英语编写,但受限于英语表达水平与有限的精力,使用中文才能保证文档的清晰、准确与全面。 对于母语非中文的读者,请尝试使用浏览器机器翻译功能。若机器翻译的效果不足以能够理解文章,请告知我。
许可 · License
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
快速上手 · Getting Started
编写自动化 Minecraft 机器人程序是 Go-MC 开发的初衷之一,
通过编写简单的客户端程序并登入官方服务端,可以验证 Go-MC 提供的其他功能性模块实现是否与原版游戏保持一致。
继承于 Go-MC 的前身项目 gomcbot,bot
包是 Go-MC 最早被开发出来的模块。
bot
包提供了一个轻量级的机器人框架,实现了获取服务器状态(Ping and List)、连接服务器登入游戏(Join Game)等功能,
还包含了一个简单的数据包 Handler 注册器,统一地管理从收到的数据包,使得对聊天、区块、窗口等功能的模块化处理成为可能。
加入游戏 · Join Game
虽然看上去简单,但是玩家连接服务器、进入游戏其实是一个有点复杂的过程。在客户端和服务端创建连接成功后,还需要经过握手、认证、加密、压缩等几个过程, 经过一系列数据包的交换,登录阶段以一个服务器发给客户端的0x02数据包结束,玩家正式开始游戏。
处理登录实在有点烦人,不过好在 bot
包可以帮你完成整个步骤,而你只需要调用一个 JoinServer
函数,就能成功加入游戏!
首先快速看一下一个最基础的 bot 程序长什么样:
package main
import (
"log"
"github.com/Tnze/go-mc/bot"
)
func main() {
client := bot.NewClient()
// ...
err := client.JoinServer("localhost:25565")
if err != nil {
log.Fatal(err)
}
log.Println("Login success")
if err = client.HandleGame(); err != nil {
log.Fatal(err)
}
}
第一步 · 创建客户端
调用 bot.NewClient()
函数会返回一个 *bot.Client
客户端对象,表示一个游戏客户端。没有需要特别注意的,
但是同一时刻一个客户端只能连接一个服务器,不能在多个 goroutine 中共用一个客户端连接服务器。
玩家加入游戏后客户端对象也会被用到,一般要把它保存在一个变量里以供后续使用,变量名一般是client
或者c
。
client := bot.NewClient()
第二步 · 设置玩家名
更改玩家名的方法比较直接,但是请注意不要和 client.Name
搞混了, client.Auth
里的才是登入时发送给服务器的认证信息。
而 client.Name
是登录成功后服务器发回来的。
client.Auth.Name = "Tnze"
第三步 · 登录正版账号
如果你想加入开启了正版验证的服务器(online-mode),还需要把 UUID 和 AccessToken 一起填上。
client.Auth = bot.Auth{
Name: "Tnze",
UUID: "58f6356e-b30c-4811-8bfc-d72a9ee99e73",
AsTk: "*******************",
}
以前 GO-MC 自带的 yggdrasil
包可以直接获取 AccessToken ,
但转为使用微软账户之后,这事儿变得有点麻烦1,并且似乎需要调用浏览器或 WebView。
将这些东西集成在 Go-MC 里并不合适,所以请使用好心人编写的工具库,已知的库如下(排名不分先后,请自行选择判断),也欢迎新的库添加到这个列表来。
Issue #106
第四步 · 加入游戏
调用 JoinServer()
并传入服务器地址即可加入游戏。服务器地址可以是域名也可以是IP地址,可以带端口号也可以不带(默认为25565)。
设计上,该地址字符串与原版游戏客户端加入服务器的地址输入框保持一致。
err := client.JoinServer("localhost:25565")
if err != nil {
log.Fatal(err)
}
第五步 · 处理游戏
在登录游戏成功后 JoinServer()
方法就会返回,如果什么都不做,程序就会结束,连接就会断开,自然也就达不到机器人的目的了。
所以在最后需要调用客户端上的 HandleGame()
方法,开始收发游戏数据包:
if err := client.HandleGame(); err != nil {
log.Fatal(err)
}
该方法会持续不断地接收数据包,并根据数据包ID执行相应的处理函数。
当任意处理函数产生了错误时 HandleGame()
就会返回,如果不希望程序就此退出,可以在处理完错误后再次调用 HandleGame()
继续游戏。
多次调用不会产生问题,设计如此,无需担心。
var err error
for {
if err = client.HandleGame(); err == nil {
panic("HandleGame never return nil")
}
log.Printf(err)
}
在 HandleGame()
返回错误时,需要区分错误是否可恢复,如果是连接断开等不可恢复错误则需要停止程序,如果是单个数据包的处理出错则可以恢复。
判断返回的错误是否可以恢复的方法是使用 errors.As()
函数,判断 err
是否是一个 bot.PacketHandlerError
,具体使用方法如下:
var err error
var perr bot.PacketHandlerError
for {
if err = client.HandleGame(); err == nil {
panic("HandleGame never return nil")
}
+ if errors.As(err, &perr) {
+ log.Print(perr)
+ } else {
+ log.Fatal(err)
+ }
}
下一步 · The next step
由于服务器的 “KeepAlive” 机制,当前编写的 bot 程序不会响应服务器的心跳包,就会在一定时间后会被服务器判定为无响应并自动踢出。
为了解决这个问题,需要使用下一章提到的 bot/basic
模块。
基础功能 · Basic Module
在 bot
包实现了最基础的登录逻辑之后,其他可选的功能都放在子包下以便按需引用,
其中一个几乎必选的子包就是 bot/basic
,从名字你就能听出来它提供的功能有多基础了。
bot/basic
包提供了以下功能:
- KeepAlive 用于服务器测试连接状态,客户端不处理会被踢出游戏
- Client Settings 向服务器发送的一些客户端设置,例如语言、主手、装备可见性、客户端名称等
- Player Info 储存玩家的实体ID,游戏模式等
- World Info 储存服务器信息,包括世界维度、视距、最大玩家总数等
整个框架以多级模块化的方式构建,bot
下的各个包并不是完全平级的关系,目前 bot/msg
和 bot/world
就都依赖 bot/basic
包。
各个模块之间的依赖在初始化时显式指定,不需要特殊关注。
使用方法 · Usage
功能模块的加载需要在 JoinServer
之前完成。
package main
import (
"log"
"github.com/Tnze/go-mc/bot"
)
var (
client *bot.Client
player *basic.Player
)
func main() {
client = bot.NewClient()
+ player = basic.NewPlayer(client, basic.DefaultSettings, basic.EventsListener{})
err := client.JoinServer("localhost:25565")
if err != nil {
log.Fatal(err)
}
log.Println("Login success")
var perr bot.PacketHandlerError
for {
if err = client.HandleGame(); err == nil {
panic("HandleGame never return nil")
}
if errors.As(err, &perr) {
log.Print(perr)
} else {
log.Fatal(err)
}
}
}
通过调用 basic.NewPlayer()
,basic
会将 player
对象注册到传入的 client
上,
client
收到响应的数据包会自动解析之后储存到 player
内部以供读取。
现在,
client
和player
是两个单独的对象,client
相当于打开的游戏程序窗口,而player
相当于加入服务器的一个游戏会话, 前者承载了网络连接与客户端设置,后者承载了游戏过程中角色的状态数据。这是一种合理的设计,但一开始并不是显而易见的。
监听事件 · Events
basic
包还提供了几个可供注册的事件,如游戏开始,血量变化,角色死亡等。
以血量变化为例,在调用 basic.NewPlayer()
传入事件响应函数即可。
func main() {
// ...
player = basic.NewPlayer(client, basic.DefaultSettings, basic.EventsListener{
HealthChange: onHealthChange,
})
// ...
}
func onHealthChange(health float32) error {
log.Printf("HealthChanged: %v", health)
return nil
}
完整的例子可在 go-mc/examples/daze 查看
聊天消息 · Chat Message
聊天消息是游戏中玩家们的主要沟通方式。一些服务器要求玩家在进入后输入指令登录。 聊天功能不仅可用于向服务器内的玩家报告当前状态,也可以用于接收玩家给的指令,是机器人所必备的重要功能之一
为 bot
包启用消息功能需要引入 bot/msg
模块,并同时依赖于 bot/basic
和 bot/playerlist
。
var (
chatHandler *msg.Manager
playerList *playerlist.PlayerList
)
playerList = playerlist.New(client)
chatHandler = msg.New(client, player, playerList, msg.EventsHandler{})
发送消息 · Send Messages
要发送消息,请调用 msg.Manager
上的 SendMessage(msg string) error
方法。
if err := chatHandler.SendMessage("Hello, world"); err != nil {
return err
}
接收消息 · Receive Messages
目前(1.19.3)在协议层面有三种消息可被接收:
- 玩家消息(Player Chat):玩家发送的消息,可以被签名确保是由玩家本人发送的。
- 系统消息(System Chat):系统发送的提示,如“Tnze加入了游戏”,分为显示在消息栏和屏幕中央两种。
- 伪消息(Disguised Chat):在服务器后台用“/say”命令发送的消息。
以下是接收聊天消息的代码示例:
chatHandler = msg.New(client, player, playerList, msg.EventsHandler{
SystemChat: onSystemMsg,
PlayerChatMessage: onPlayerMsg,
DisguisedChat: onDisguisedMsg,
})
func onSystemMsg(msg chat.Message, overlay bool) error {
log.Printf("System: %v", c)
return nil
}
func onPlayerMsg(msg chat.Message, validated bool) error {
log.Printf("Player: %s", msg)
return nil
}
func onDisguisedMsg(msg chat.Message) error {
log.Printf("Disguised: %v", msg)
return nil
}
消息处理 · Handle Message
Minecraft 使用 JSON 格式储存结构化的聊天消息,在消息中可以包含颜色、加粗、斜体等格式,以及点击事件、鼠标悬浮显示内容等功能。
在 go-mc/chat
包提供了操作聊天消息的 API,请查看处理聊天消息 · Chat。
网络连接 · Network
本章假设读者理解 TCP 协议的工作方式并有一定使用经验。
关于 Minecraft 网络协议的详细说明可阅读 https://wiki.vg/Protocol。
众所周知,Minecraft Java 版使用的是基于 TCP 的网络传输协议,而 TCP 具有点对点、传输可靠、保证顺序、不保留消息边界等特点。 除了消息边界以外,这些特点同样适用于 Minecraft 协议,请在编写代码时时刻谨记。
Minecraft 在 TCP 的基础上构造了基于长度的分包、基于 zlib 算法的压缩以及基于 RSA 和 AES/CFB8 的加密等协议。
如需收发网络包,请导入 Go-MC 提供的网络库:
import (
"github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet"
)
特殊情况下,如需同时使用标准库的 net
,请为 Go-MC 的网络库设置别名:
import (
"net"
mcnet "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet"
)
本章提到的 net
包如无特殊说明均指的是 go-mc/net
包,而不是 Go 标准库的 net
包。
创建连接 · Create Connection
使用 go-mc/net
包创建连接的方式与使用 Go 标准库创建 TCP 连接的方式非常类似。
客户端 · Client
以下代码连接运行于 localhost:25565
的服务器:
conn, err := net.DialMC("localhost:25565")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
服务端 · Server
以下代码在所有本机 IP 地址的 25565 端口监听连接:
listener, err := net.ListenMC("0.0.0.0:25565")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
go handle(&conn)
}
发送数据包 · Sending Packets
要发送数据包,你需要有一个网络连接 net.Conn(conn)
与一个数据包 pk.Packet(p)
。
通过简单地调用 conn.WritePacket(p)
即可将数据包发送。
关于如何获得 pk.Packet
,请参考下一节数据打包 · Packing。
err := conn.WritePacket(p)
if err != nil {
log.Print(err)
return
}
接收数据包 · Receiving Packets
接收数据包与发送数据包类似,你同样需要有一个 net.Conn(conn)
与一个 pk.Packet(p)
。
不同的是这次的操作是从连接读取数据并写入数据包。
var p pk.Packet
err := conn.ReadPacket(&p)
if err != nil {
log.Print(err)
return
}
之所以设计为需要传入 &p
而不是直接返回一个新的 p
,是为了方便复用 pk.Packet
对象。
当循环接收数据包时,每个连接只使用一个 pk.Packet
,可以复用内部缓冲区,起到减少内存分配,减轻 GC 压力的作用。
var p pk.Packet
for {
err := conn.ReadPacket(&p)
if err != nil {
log.Print(err)
break
}
handle(p) // 需要注意 p 只在下次 ReadPacket 调用前有效
}
数据打包 · Packing
当连接建立成功,在 net.Conn
实例上能调用的方法有三个:
- 发送数据
WritePacket(p pk.Packet) error
- 接收数据
ReadPacket(p *pk.Packet) error
- 关闭连接
Close() error
在 Login 阶段还需要按协议调用
SetCipher
和SetThreshold
,这里不做讨论。
无论是发送数据还是接收数据,Minecraft 协议都是以数据包(pk.Packet
)为单位处理的。
包长度、加密、压缩等传输细节1 Go-MC 都已经实现并封装到 pk.Packet
结构体内了,用户直接使用即可。
结构体 pk.Packet
定义如下:
type Packet struct {
ID int32
Data []byte
}
ID
是一个枚举值,表示数据包的功能,Data
的格式也需要根据它判断。可用的值在 data/packetid
中找到。
Data
逻辑上包含了一个或多个数据项,例如客户端在玩家移动时可能会发送的 Move 包可能格式如下:
Field Name | Field Type | Notes |
---|---|---|
X | Double | Absolute position. |
Feet Y | Double | Absolute feet position, normally Head Y - 1.62. |
Z | Double | Absolute position. |
On Ground | Boolean | True if the client is on the ground, false otherwise. |
即 Data
包含4个数据字段:三个 Double 和一个 Boolean。
所有的字段类型都定义在 net/packet
包,例如 pk.Double
、pk.Boolean
。
这些类型都实现了 pk.Field
接口,你可以调用它们的 ReadFrom
和 WriteTo
方法。
以下代码可以用来生成一个这样的数据包。
var buffer bytes.Buffer
_, _ = pk.Double(x).WriteTo(&buffer)
_, _ = pk.Double(y).WriteTo(&buffer)
_, _ = pk.Double(z).WriteTo(&buffer)
_, _ = pk.Boolean(onGround).WriteTo(&buffer)
p := pk.Packet {
ID: packetid.ServerboundMovePlayerPos,
Data: buffer.Bytes(),
}
操作起来有点繁琐,好在 Go-MC 为这样的操作提供了一个帮助函数:pk.Marshal()
。经过改进的代码如下:
p := pk.Marshal(
packetid.ServerboundMovePlayerPos,
pk.Double(x),
pk.Double(y),
pk.Double(z),
pk.Boolean(onGround),
)
反过来,如果服务器想接收一个这样的数据包,那么需要用到 ReadFrom
方法,用笨办法就是如下这样:
var (
x pk.Double
y pk.Double
z pk.Double
onGround pk.Boolean
)
r := bytes.NewReader(p.Data)
_, _ = x.ReadFrom(r)
_, _ = y.ReadFrom(r)
_, _ = z.ReadFrom(r)
_, _ = onGround.ReadFrom(r)
加上错误处理更加繁琐了!好在 Go-MC 也提供了一个帮助函数:p.Scan()
。经过改进的代码如下:
var (
x pk.Double
y pk.Double
z pk.Double
onGround pk.Boolean
)
_ = p.Scan(&x, &y, &z, &onGround)
在实际使用中,一般需要根据情况选择是否使用帮助函数,请自行判断。
使用 pk.Array
在一些数据包格式中,会存在 Array of X
类型的数组字段。
这些数组的前一个字段通常是一个 VarInt
类型的长度字段, Go-MC 提供了一个帮助类型来处理这种常见情况。
以 Commands 包为例,目前该数据包格式如下:
Field Name | Field Type | Notes |
---|---|---|
Count | VarInt | Number of elements in the following array. |
Nodes | Array of Node | An array of nodes. |
Root index | VarInt | Index of the root node in the previous array. |
我们假设你已经定义好 Node
类型并为其实现了 pk.Field
接口,为了接收这个数据包中的 Nodes 数组,
我们可以采用以下繁琐的代码:
var (
count pk.VarInt
nodes []Node
root pk.VarInt
)
r := bytes.NewReader(p.Data)
_, _ = count.ReadFrom(r)
nodes = make([]Node, count)
for i := range nodes {
_, _ = nodes[i].ReadFrom(r)
}
root.ReadFrom(r)
为了使用 p.Scan()
简化工作量,我们需要用到 pk.Array()
函数:
var (
nodes []Node
root pk.VarInt
)
_ = p.Scan(
pk.Array(&nodes),
&root,
)
使用 pk.Array()
,会自动处理数组长度的 VarInt
。返回值实现 pk.Field
接口,可用于 pk.Marshal()
与 pk.Scan()
。
注意,当切片的元素类型不支持相应调用的 WriteTo
和 ReadFrom
方法时,会 panic。
请确保将 pk.Array()
的返回值用作 FieldEncoder
时,切片元素类型也实现 FieldEncoder
,
将 pk.Array()
的返回值用作 FieldDecoder
时,切片元素类型也实现 FieldDecoder
。
使用 pk.Option
一些数据包格式中,会存在 Optional X
类型的可选字段。顾名思义,这种字段在数据包中是可选的。
可选字段是否存在需要根据上下文进行判断,上下文通常指其前面的 Boolean 值,例如:
Field Name | Field Type | Notes |
---|---|---|
Is Signed | Boolean | |
Signature | Optional String | Exist only if Is Signed is true |
读取该数据包的伪代码如下:
IsSigned = ReadBoolean()
if IsSigned {
Signature = ReadString()
}
为了能在 p.Scan()
函数调用中提供这样的判断逻辑,Go-MC 提供了四个帮助类型:pk.Option
、 pk.OptionDecoder
、 pk.OptionEncoder
和 pk.Opt
。
pk.Opt
是一个一般不会用到的原始类型,如需使用请自行阅读注释及源码,在此不做说明。
pk.Option
是一个泛型类型,有两个泛型参数:T
和 P
,其中后者是前者的指针类型。
T
必须满足 pk.FieldEncoder
接口,P
必须满足 FieldDecoder
接口。
例如:pk.Option[pk.String, *pk.String]
,pk.Option[pk.VarInt, *pk.VarInt]
等,都是合法的类型。
需要手动指定 P
是因为当前 Go 编译器泛型实现不够完善2,在 Go 支持推导结构体泛型参数类型之后,
上面的两个例子就可以简单写成 pk.Option[pk.String]
和 pk.Option[pk.VarInt]
了。
pk.OptionDecoder
和 pk.OptionEncoder
是两个变体,分别去掉了 对T
的约束 和 对P
的约束,很好理解。
以下给出读写上面例子数据包的完整代码
var Signature pk.Option[pk.String, *pk.String]
_ = p.Scan(&Signature)
编解码 NBT
Go-MC 提供了一个功能完善的 NBT 操作库,可以用于方便、高效地处理各种NBT结构。这里会介绍这个 NBT 库提供的核心API。
使用 NBT 库,需要导入以下包:
import "github.com/Tnze/go-mc/nbt"
该 NBT 库的用法与标准库的 encoding/json
库比较相似,查看文档及示例很容易理解,这里是一些注意事项。
不同类型的Go变量会被转换为不同的 NBT 标签,对应关系如下:
Go 类型 | NBT 标签 |
---|---|
bool, int8, uint8 | TagByte |
int16, uint16 | TagShort |
int32, uint32 | TagInt |
int64, uint64 | TagLong |
float32 | TagFloat |
float64 | TagDouble |
string | TagString |
struct, map | TagCompound |
[]bool, []int8, []uint8 | TagByteArray |
[]int32, []uint32 | TagIntArray |
[]int64, []uint64 | TagLongArray |
[]T | TagList |
NBT库内部使用反射来实现以上功能,但是实现了 nbt.Marshaler
和 nbt.Unmarshaler
接口的值不会被反射,NBT 库会调用它们自身的实现。
你可以通过实现这两个接口实现自定义类型的支持。NBT库也有三个特殊类型实现了这两个接口,你可以利用它们完成一些特殊功能:
nbt.RawMessage
- “原始数据”类型,NBT 数据将被原样保存,可以容纳任意合法的NBT数据。nbt.StringifiedMessage
- 以 S-NBT 格式保存数据,可以与string
相互转换。dynbt.Value
- “动态”类型,可以保存任意数据,并且提供了方便的API用于操控 NBT。
结构体标签和选项 · Supported Struct Tags and Options
你可以给结构体标记 Tag 以指定 nbt 库如何将其编码为 NBT 及 如何将 NBT 解码到其之上。目前支持的标签有以下两个:
nbt
- 用于指定名称和参数nbtkey
- 只用于指定名称 (可以包含逗号,
)
The nbt
tag
大多数情况下,你只需要用这个 tag 来设置 NBT 标签名。
nbt
tag 的格式: <nbt tag>[,opt]
.
这是一个逗号分隔的列表。 第一项是标签名,其余部分为选项。
像这样:
type MyStruct struct {
Name string `nbt:"name"`
}
使用 -
可以让 NBT 库忽略某个字段:
type MyStruct struct {
Internal string `nbt:"-"`
}
使用 omitempty
可以让 NBT 库仅在为零值时忽略某个字段:
type MyStruct struct {
Name string `nbt:"name,omitempty"`
}
类型为 []byte
, []int32
和 []int64
的值默认将被分别编码为 TagByteArray
, TagIntArray
和 TagLongArray
。
你可以通过 list
标记将它们指定为 TagList
:
type MyStruct struct {
Data []byte `nbt:"data,list"`
}
The nbtkey
tag
官方 JSON 标准库有一个严重的问题:无法指定包含逗号的key(逗号后面的会被识别为其他选项)
(例如 {"a,b" : "c"}
)
Go官方是非常自傲的,仅仅因为他们“觉得”key里不应该有逗号,他们就不让你加逗号,尽管JSON标准是完全支持的。
但是我们不一样,对这种需求我们提供了支持的方案:
type MyStruct struct {
AB string `nbt:",omitempty" nbtkey:"a,b"`
}
使用 S-NBT · Stringified NBT
这是一种“人类可读”的NBT表示形式,也是在游戏原版命令中输入任何NBT数据使用的格式。
动态 NBT · Dynamic NBT
这是另一种风格的 NBT 库,它允许你动态地添加、删除和查询数据,而无需预先定义数据类型或者操纵讨厌的 map[string]any
!
聊天 · Chat
Go-MC 在 go-mc/chat
包的提供了操作聊天消息的 API。
import "github.com/Tnze/go-mc/chat"
其中 chat.Message
是主要结构,每个 chat.Message
结构体可表示一条聊天消息。
使用 · Usage
该结构体实现了 pk.Filed
接口,可以直接用于网络包字段的发送与接收:
func HandleDisguisedChatMessagePacket(p pk.Packet) error {
var (
Message chat.Message
ChatType pk.VarInt
ChatTypeName chat.Message
TargetName pk.Option[chat.Message, *chat.Message]
)
err := p.Scan(&Message, &ChatType, &ChatTypeName, &TargetName)
// ...
}
该结构体通过与 json
标准库交互,也可以实现和 []byte
互转:
func ToJson(msg chat.Message) (data []byte, err error) {
return json.Marshal(msg)
}
func FromJson(data []byte) (msg chat.Message, err error) {
err = json.Unmarshal(data, &msg)
return
}
通过 fmt
库可以直接输出带样式的消息文本,原理是 .String()
方法将颜色等样式转换为ANSI 转义序列。
所以在类 Unix 系统上的大部分终端中,都支持通过 fmt.Print(msg)
直接实现输出带样式的聊天消息。
注意 为了支持 Windows 系统 ,需要使用 go-colorable 这个库。
如果你需要将 chat.Message
转换为 不包含 ANSI 转义序列 的纯文本,则可以选择调用 .ClearString()
方法。
构造 · Construction
聊天消息按使用到的特性可分为如下三种
- 普通消息
- 附加消息
- 翻译消息
普通消息 · Normal
普通消息的格式很简单,仅仅是包含了 .Text
字段(而不包含 .Translate
,.With
,.Extra
等)。
msg := chat.Message {
Text: "hello, world",
}
hello, world
普通消息像其他消息一样,也可以包含一些样式,包括:
- 加粗:Bold
- 倾斜:Italic
- 下划线:UnderLined
- 删除线:StrikeThrough
- 随机化:Obfuscated
- 指定字体:Font
- 指定颜色:Color
附加消息 · Extra
附加消息就是将一个或多个另外的额外消息连接在主消息之后。可以用于对消息不同部分设置不同样式。
附加的部分都放在 .Extra
字段中:
msg := chat.Message {
Text: "hello",
Extra: []chat.Message {
chat.Message {
Text: ", ",
},
chat.Message {
Text: "world",
},
},
}
hello, world
翻译消息 · Translate
翻译消息与普通消息完全不同,它 不包含 .Text
字段 ,取而代之的是 .Translate
和 .With
。
翻译消息用于向设置不同语言的客户端展示不同的消息,其原理可以简单理解为格式化字符串(printf
)。
例如,玩家加入服务器时服务器会发送一条 .Translate == "multiplayer.player.joined"
的消息。
该消息在中文客户端中内容为 "%s加入了游戏"
,而在英语客户端中则为 %s joined the game
。而玩家名则通过 .With
字段提供。
以下是一个简单的例子:
msg := chat.Message {
Trasnlate: "multiplayer.player.joined",
With: []chat.Message {
chat.Message {
Text: "Tnze",
},
},
}
Tnze加入了游戏
在这里 Tnze
会被填充到翻译消息的 %s
处,最终在中文客户端中显示为 Tnze加入了游戏
,而在英语客户端中显示为 Tnze joined the game
。
所有可用的消息都可以在 go-mc/data/lang
下找到。
如果你想用 Go-MC 实现一个自己的服务端,但是又想为自定义消息提供多语言功能,默认提供的翻译消息列表可能就不够用了。 你可以通过读取客户端发送的
ClientInformation
包内的Local
字段获知客户端设置的语言,并在服务端进行翻译工作。
快捷方式 · Shortcrust
Go-MC 提供了一些常用的构造函数,用来快速生成你想要的消息结构体。我相信,这些函数不需要说明也能很容易的被理解和使用。
chat.Text("hello, world")
chat.Text("hello").Append(chat.Text(", "), chat.Text("world"))
chat.Text("Waring").SetColor(chat.Yellow)
chat.TranslateMsg("multiplayer.player.joined", chat.Text("Tnze"))
更多 · More
聊天消息还支持悬停事件、点击事件等功能,详细的使用方法请参考 https://wiki.vg/Chat 以及 go-mc/chat
包源代码。
编写服务端 · Writing Server
Go-MC 提供了一个轻量级服务端框架,可以用来快速实现一个服务端。
架构说明
从协议角度,玩家进入服务器游玩一共分为三个阶段:
名称 | 功能 |
---|---|
Ping & List | 玩家进入服务器前,展示服务器信息与在线玩家数 |
登录 | 玩家进入服务器游玩,处理登录协议,设置网络连接的压缩与加密 |
游玩 | 玩家登录成功进入游戏,处理游戏逻辑与玩家交互 |
Go-MC 围绕这三个阶段设计,通过接口提供了一个自由度极高的服务器框架。
以下是 Go-MC 提供的三个主要接口:
- LoginHandler:用于提供登录协议,默认提供 Mojang 登录验证的实现,用户可以通过编写自己的 LoginHandler 来实现自定义登录验证。
- ListPingHandler:为外部提供服务器状态信息的查询功能,用户可以通过编写自己的 ListPingHandler 来实现显示自定义的服务器状态信息。
- GamePlay:最有趣的部分,留给用户自行实现,用户可以参考 https://github.com/go-mc/server 由 Tnze 编写的版本。
//TODO