package ark import ( "bytes" "errors" "net" "regexp" "time" ) // Info is what a server tells the Steam server browser (A2S_INFO). type Info struct { Name string `json:"name"` Map string `json:"map"` Game string `json:"game"` Version string `json:"version,omitempty"` Players int `json:"players"` MaxPlayers int `json:"max_players"` PingMS int64 `json:"ping_ms"` } var a2sRequest = []byte("\xff\xff\xff\xffTSource Engine Query\x00") // ARK puts its version in the server name: "My Server - (v349.7)". var nameVersion = regexp.MustCompile(`\(v(\d+(?:\.\d+)*)\)`) // Query asks the game's query port (UDP, usually 27015) who is on the server, without needing the // RCON password. func Query(addr string, timeout time.Duration) (*Info, error) { conn, err := net.Dial("udp", addr) if err != nil { return nil, err } defer conn.Close() start := time.Now() conn.SetDeadline(start.Add(timeout)) if _, err := conn.Write(a2sRequest); err != nil { return nil, err } buf := make([]byte, 1400) n, err := conn.Read(buf) if err != nil { return nil, err } if n >= 9 && buf[4] == 0x41 { // challenge: ask again with the number it gave req := append(append([]byte{}, a2sRequest...), buf[5:9]...) if _, err := conn.Write(req); err != nil { return nil, err } if n, err = conn.Read(buf); err != nil { return nil, err } } info, err := parseInfo(buf[:n]) if err != nil { return nil, err } info.PingMS = time.Since(start).Milliseconds() return info, nil } func parseInfo(b []byte) (*Info, error) { if len(b) < 6 || !bytes.Equal(b[:4], []byte{0xff, 0xff, 0xff, 0xff}) || b[4] != 0x49 { return nil, errors.New("not an A2S_INFO reply") } r := b[6:] // skip the header and the protocol byte str := func() string { i := bytes.IndexByte(r, 0) if i < 0 { s := string(r) r = nil return s } s := string(r[:i]) r = r[i+1:] return s } var in Info in.Name, in.Map = str(), str() str() // folder in.Game = str() if len(r) < 9 { return nil, errors.New("short A2S_INFO reply") } in.Players, in.MaxPlayers = int(r[2]), int(r[3]) r = r[9:] // app id (2), players, max, bots, type, environment, visibility, VAC in.Version = str() if m := nameVersion.FindStringSubmatch(in.Name); m != nil { in.Version = m[1] } return &in, nil }