Files
cellbot/internal/plugins/mcstatus/ping.go

142 lines
3.5 KiB
Go
Raw Normal View History

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
}