142 lines
3.5 KiB
Go
142 lines
3.5 KiB
Go
|
|
package mcstatus
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
mcnet "github.com/Tnze/go-mc/net"
|
||
|
|
pk "github.com/Tnze/go-mc/net/packet"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ServerStatus 服务器状态信息
|
||
|
|
type ServerStatus struct {
|
||
|
|
Host string
|
||
|
|
Port int
|
||
|
|
Latency int64 // 延迟(毫秒)
|
||
|
|
Version Version
|
||
|
|
Players Players
|
||
|
|
Description string // MOTD
|
||
|
|
Favicon string // Base64 编码的图标
|
||
|
|
}
|
||
|
|
|
||
|
|
// Version 版本信息
|
||
|
|
type Version struct {
|
||
|
|
Name string
|
||
|
|
Protocol int
|
||
|
|
}
|
||
|
|
|
||
|
|
// Players 玩家信息
|
||
|
|
type Players struct {
|
||
|
|
Online int
|
||
|
|
Max int
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ping 查询 Minecraft 服务器状态
|
||
|
|
func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) {
|
||
|
|
// 连接服务器
|
||
|
|
startTime := time.Now()
|
||
|
|
conn, err := mcnet.DialMC(fmt.Sprintf("%s:%d", host, port))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to connect: %w", err)
|
||
|
|
}
|
||
|
|
defer conn.Close()
|
||
|
|
|
||
|
|
latency := time.Since(startTime).Milliseconds()
|
||
|
|
|
||
|
|
// 发送握手包
|
||
|
|
handshake := pk.Marshal(
|
||
|
|
0x00, // Handshake packet ID
|
||
|
|
pk.VarInt(47), // Protocol version (1.8+)
|
||
|
|
pk.String(host),
|
||
|
|
pk.UnsignedShort(port),
|
||
|
|
pk.VarInt(1), // Next state: Status
|
||
|
|
)
|
||
|
|
if err := conn.WritePacket(handshake); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to send handshake: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 发送状态请求
|
||
|
|
request := pk.Marshal(0x00) // Status request packet ID
|
||
|
|
if err := conn.WritePacket(request); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to send status request: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 读取响应
|
||
|
|
var response pk.Packet
|
||
|
|
if err := conn.ReadPacket(&response); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解析响应
|
||
|
|
var statusJSON pk.String
|
||
|
|
if err := response.Scan(&statusJSON); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解析 JSON 响应
|
||
|
|
var statusData map[string]interface{}
|
||
|
|
if err := json.Unmarshal([]byte(statusJSON), &statusData); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
status := &ServerStatus{
|
||
|
|
Host: host,
|
||
|
|
Port: port,
|
||
|
|
Latency: latency,
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解析 description
|
||
|
|
if desc, ok := statusData["description"].(map[string]interface{}); ok {
|
||
|
|
if text, ok := desc["text"].(string); ok {
|
||
|
|
status.Description = text
|
||
|
|
} else if text, ok := desc["extra"].([]interface{}); ok && len(text) > 0 {
|
||
|
|
// 处理 extra 数组
|
||
|
|
var descText strings.Builder
|
||
|
|
for _, item := range text {
|
||
|
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
||
|
|
if text, ok := itemMap["text"].(string); ok {
|
||
|
|
descText.WriteString(text)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
status.Description = descText.String()
|
||
|
|
}
|
||
|
|
} else if desc, ok := statusData["description"].(string); ok {
|
||
|
|
status.Description = desc
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解析 version
|
||
|
|
if version, ok := statusData["version"].(map[string]interface{}); ok {
|
||
|
|
if name, ok := version["name"].(string); ok {
|
||
|
|
status.Version.Name = name
|
||
|
|
}
|
||
|
|
if protocol, ok := version["protocol"].(float64); ok {
|
||
|
|
status.Version.Protocol = int(protocol)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解析 players
|
||
|
|
if players, ok := statusData["players"].(map[string]interface{}); ok {
|
||
|
|
if online, ok := players["online"].(float64); ok {
|
||
|
|
status.Players.Online = int(online)
|
||
|
|
}
|
||
|
|
if max, ok := players["max"].(float64); ok {
|
||
|
|
status.Players.Max = int(max)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解析 favicon
|
||
|
|
if favicon, ok := statusData["favicon"].(string); ok {
|
||
|
|
// 移除 data:image/png;base64, 前缀
|
||
|
|
if strings.HasPrefix(favicon, "data:image/png;base64,") {
|
||
|
|
status.Favicon = strings.TrimPrefix(favicon, "data:image/png;base64,")
|
||
|
|
} else {
|
||
|
|
status.Favicon = favicon
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return status, nil
|
||
|
|
}
|