package controllers import ( "InfantrySkillCalculator/models" "InfantrySkillCalculator/utils" "encoding/json" "errors" "fmt" "io" "math" "net" "net/http" "sort" "time" "github.com/gin-gonic/gin" ) // GetScoreByPlayerID GET /score/:player_id func GetScoreByPlayerID(c *gin.Context) { var player models.Player if err := models.DB.Where("id = ?", c.Param("player_id")).First(&player).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"}) utils.Logger.Errorf("[SCORE] Player not found: %s", err.Error()) return } score, err := models.PlayerCache.GetScore(player.ID) var statusCode = http.StatusOK if err != nil || score == -1 { score, statusCode = GetPlayerScoreNew(player.Name) if score == score && score != -1 { // not NaN if err := models.PlayerCache.SetScore(player.ID, score); err != nil { utils.Logger.Errorf("[SCORE] Failed to cache player score: %s", err.Error()) return } } } switch statusCode { case 200: c.String(200, fmt.Sprintf("%.2f", score)) case 404: c.String(200, "") case 503, 504, 408: c.String(statusCode, "") default: c.String(statusCode, "") utils.Logger.Warnf("[SCORE] Invalid request! Player: %s, Score: %f", player.Name, score) } } // GetScoreByPlayerName POST /score/:player_name func GetScoreByPlayerName(c *gin.Context) { playerName := c.Param("player_name") score, statusCode := GetPlayerScoreNew(playerName) switch statusCode { case 200: c.String(200, fmt.Sprintf("%.2f", score)) case 404: c.String(200, "Spieler nicht gefunden!") case 503, 504, 408: c.String(statusCode, "Tracker nicht erreichbar!") default: c.String(statusCode, "Ungültige Abfrage!") utils.Logger.Warnf("[SCORE] Invalid request! Player: %s, Score: %f", playerName, score) } } func GetPlayerScore(playerName string) (float32, int) { playerData, statusCode := getPlayerData(playerName) if statusCode != 200 { return -1, statusCode } return calcPlayerScore(playerData), statusCode } func GetPlayerScoreNew(playerName string) (float32, int) { playerData, statusCode := getPlayerData(playerName) //if statusCode == 408 { // retry once // playerData, statusCode = getPlayerData(playerName) //} if statusCode != 200 { return -1, statusCode } return calcPlayerScoreNew(playerData), statusCode } func getPlayerData(playerName string) (*models.TrackerDataJSON, int) { c := http.Client{ Timeout: 14 * time.Second, // Cloudflare timeout is 15s } reqUri := "https://api.gametools.network/bf2042/stats/?raw=false&format_values=false&name=" + playerName + "&platform=pc" req, err := http.NewRequest("GET", reqUri, nil) if err != nil { utils.Logger.Errorf("[SCORE] Failed to create request: %s", err.Error()) return nil, 0 } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36") req.Header.Set("Accept-Encoding", "gzip, deflate, br") res, err := c.Do(req) if err != nil { var e net.Error if errors.As(err, &e) && e.Timeout() { utils.Logger.Errorf("[SCORE] Request timeout!") return nil, 408 } utils.Logger.Errorf("[SCORE] Failed to send request: %s", err.Error()) return nil, 0 } defer func(Body io.ReadCloser) { _ = Body.Close() }(res.Body) if res.StatusCode == 404 { utils.Logger.Errorf("[SCORE] User does not exist!") return nil, res.StatusCode } else if res.StatusCode == 503 || res.StatusCode == 504 { utils.Logger.Errorf("[SCORE] Service unavailable!") return nil, res.StatusCode } else if res.StatusCode != 200 { utils.Logger.Errorf("[SCORE] Status code error: %d %s", res.StatusCode, res.Status) return nil, res.StatusCode } encoding := res.Header.Get("Content-Encoding") reader, err := utils.DecompressResponseBody(encoding, res.Body) if err != nil { utils.Logger.Errorf("[SCORE] Failed to decompress response body: %s", err.Error()) return nil, 0 } defer reader.Close() var response models.TrackerDataJSON body, err := io.ReadAll(reader) if err != nil { utils.Logger.Errorf("[SCORE] Failed to read response body: %s", err.Error()) return nil, 0 } if err := json.Unmarshal(body, &response); err != nil { utils.Logger.Errorf("[SCORE] Failed to deserialize tracker API response: %s", err) return nil, 0 } return &response, 200 } func calcPlayerScoreNew(playerData *models.TrackerDataJSON) float32 { const KdFactor = 0.5 const KpmFactor = 0.3 const ObjectiveFactor = 0.15 const SupportFactor = 0.05 const NormalizeFactor = 1.3 cleanedKills := playerData.DivKills.ADS + playerData.DivKills.Hip - playerData.DivKills.AI kd := float64(cleanedKills) / float64(playerData.Deaths) kpm := float64(cleanedKills) / float64(playerData.SecondsPlayed/60.0) objective := float64(playerData.XP[0].Ribbons.Objective) / float64(playerData.XP[0].Ribbons.Total) support := float64(playerData.XP[0].Ribbons.Support+playerData.XP[0].Ribbons.Squad) / float64(playerData.XP[0].Ribbons.Total) score := (kd * KdFactor) + (kpm * KpmFactor) + (objective * ObjectiveFactor) + (support * SupportFactor) score *= NormalizeFactor return float32(score) } func calcPlayerScore(playerData *models.TrackerDataJSON) float32 { gameMetrics := GetGameMetric("BF2042") if gameMetrics == nil { utils.Logger.Errorf("[SCORE] No game metrics specified for '%s'", "BF2042") return -1 } normalizeFactor := gameMetrics.NormalizeFactor topWeaponCount := gameMetrics.TopWeaponCount sort.SliceStable(playerData.Weapons, func(i, j int) bool { return playerData.Weapons[i].Kills > playerData.Weapons[j].Kills }) var top []models.Weapon for _, weapon := range playerData.Weapons { if len(top) >= topWeaponCount || weapon.Kills < 100 { break } acc := (float64(weapon.ShotsHit) / float64(weapon.ShotsFired)) * 100 kpm := weapon.KPM weaponMetrics := FindWeaponMetric(gameMetrics.WeaponMetrics, weapon.Type) if weaponMetrics == nil { utils.Logger.Errorf("[SCORE] No weapon metrics specified for '%s'", weapon.Type) continue } accFactor := weaponMetrics.AccuracyFactor kpmFactor := weaponMetrics.KpmFactor acc /= accFactor kpm /= kpmFactor weapon.Accuracy = acc weapon.KPM = kpm top = append(top, weapon) } sumAcc := 0.0 sumKpm := 0.0 for _, w := range top { sumAcc += w.Accuracy sumKpm += w.KPM } accAvg := sumAcc / float64(len(top)) kpmAvg := sumKpm / float64(len(top)) score := math.Round(((accAvg*kpmAvg)/normalizeFactor)*100) / 100 return float32(score) }