package models import ( "bufio" "bytes" "errors" "fmt" "net" "regexp" "strconv" "strings" "time" ) var ( timeLayout = "Mon Jan 2 15:04:05 2006" ) type StatusProvider interface { GetServerStatus(string, string, time.Duration) (*CoD4ServerStatus, error) } type CoD4Server struct { server string port string timeout time.Duration } type CoD4ServerStatus struct { raw string serverData map[string]string Name string MapStartTime time.Time MapName string MaxPlayers int Score []Score meta map[string]interface{} } type CoD4UdpClient struct{} func (CoD4UdpClient) GetServerStatus(server, port string, timeout time.Duration) (*CoD4ServerStatus, error) { if server == "" || port == "" { return nil, errors.New("server or port is empty") } c := &CoD4Server{ server: server, port: port, timeout: timeout, } data, err := c.receiveData() if err != nil { return nil, err } return parseServerData(data) } func (c *CoD4Server) receiveData() (string, error) { address := fmt.Sprintf("%s:%s", c.server, c.port) conn, err := net.DialTimeout("udp", address, c.timeout) if err != nil { return "", fmt.Errorf("could not connect to server: %s", err) } defer conn.Close() conn.SetDeadline(time.Now().Add(c.timeout)) _, err = conn.Write([]byte("\xFF\xFF\xFF\xFFgetstatus\x00")) if err != nil { return "", fmt.Errorf("failed to send data to server: %s", err) } var buf bytes.Buffer response := make([]byte, 8192) reader := bufio.NewReader(conn) for { n, err := reader.Read(response) if err != nil { break } buf.Write(response[:n]) } data := buf.String() if len(strings.TrimSpace(data)) == 0 { return "", fmt.Errorf("no data received from server: %s", err) } return data, nil } func parseServerData(data string) (*CoD4ServerStatus, error) { lines := strings.Split(data, "\n") if len(lines) < 2 { return nil, fmt.Errorf("insufficient data received") } c := &CoD4ServerStatus{ raw: data, serverData: make(map[string]string), meta: make(map[string]interface{}), } tempPlayers := lines[2:] tempData := strings.Split(lines[1], "\\") for i := 1; i < len(tempData)-1; i += 2 { c.serverData[tempData[i]] = tempData[i+1] } startTime, err := time.ParseInLocation(timeLayout, c.serverData["g_mapStartTime"], time.Local) if err != nil { return nil, err } c.serverData["sv_hostname"] = colorCode(c.serverData["sv_hostname"]) c.Name = c.serverData["sv_hostname"] c.serverData["_Maps"] = strings.Join(strings.Split(c.serverData["_Maps"], "-"), ",") c.MapName = c.serverData["mapname"] c.MaxPlayers, err = strconv.Atoi(c.serverData["sv_maxclients"]) if err != nil { return nil, err } c.MapStartTime = startTime for _, playerLine := range tempPlayers { if len(strings.TrimSpace(playerLine)) > 1 { temp := strings.Fields(playerLine) if len(temp) >= 3 { playerName := strings.Trim(playerLine, `"`) scoreValue, err := strconv.Atoi(temp[0]) if err != nil { return nil, err } pingValue, err := strconv.Atoi(temp[1]) if err != nil { return nil, err } c.Score = append(c.Score, Score{ Name: playerName[strings.Index(playerName, "\"")+1:], Score: scoreValue, Ping: pingValue, }) } } } return c, nil } func colorCode(str string) string { str += "^" colorMap := map[string]string{ "0": "#000000", "1": "#F65A5A", "2": "#00F100", "3": "#EFEE04", "4": "#0F04E8", "5": "#04E8E7", "6": "#F75AF6", "7": "#FFFFFF", "8": "#7E7E7E", "9": "#6E3C3C", } re := regexp.MustCompile(`\^(\d)`) str = re.ReplaceAllStringFunc(str, func(m string) string { code := string(m[1]) if color, ok := colorMap[code]; ok { return fmt.Sprintf(``, color) } return m }) // Close open span tags str += "^" reClose := regexp.MustCompile(`\^(.*?)\^`) str = reClose.ReplaceAllString(str, "$1") return str[:len(str)-1] }