package slapshotapi import ( "fmt" "strings" "github.com/pkg/errors" ) // DetectTampering analyzes 3 period logs for modification signs. // Returns (isTampering bool, reason string, error). func DetectTampering(logs []*MatchLog) (bool, string, error) { if len(logs) != 3 { return false, "", errors.New("exactly 3 period logs are required") } reasons := []string{} // Check metadata consistency tampered, reason, err := ValidateMetadataConsistency(logs) if err != nil { return false, "", errors.Wrap(err, "ValidateMetadataConsistency") } if tampered { reasons = append(reasons, reason) } // Check score progression tampered, reason, err = ValidateScoreProgression(logs) if err != nil { return false, "", errors.Wrap(err, "ValidateScoreProgression") } if tampered { reasons = append(reasons, reason) } // Check player consistency tampered, reason, err = ValidatePlayerConsistency(logs) if err != nil { return false, "", errors.Wrap(err, "ValidatePlayerConsistency") } if tampered { reasons = append(reasons, reason) } if len(reasons) > 0 { return true, strings.Join(reasons, "; "), nil } return false, "", nil } // ValidateMetadataConsistency checks that arena, match_length, and // custom_mercy_rule are consistent across all periods, and warns if any // period has periods_enabled set to "False" (periods disabled). func ValidateMetadataConsistency(logs []*MatchLog) (bool, string, error) { if len(logs) != 3 { return false, "", errors.New("exactly 3 period logs are required") } ref := logs[0] inconsistencies := []string{} for i := 1; i < len(logs); i++ { log := logs[i] if log.Arena != ref.Arena { inconsistencies = append(inconsistencies, fmt.Sprintf("arena differs in period %d (%q vs %q)", i+1, log.Arena, ref.Arena)) } if log.MatchLength != ref.MatchLength { inconsistencies = append(inconsistencies, fmt.Sprintf("match_length differs in period %d (%s vs %s)", i+1, log.MatchLength, ref.MatchLength)) } if log.CustomMercyRule != ref.CustomMercyRule { inconsistencies = append(inconsistencies, fmt.Sprintf("custom_mercy_rule differs in period %d (%s vs %s)", i+1, log.CustomMercyRule, ref.CustomMercyRule)) } } // Warn if any period has periods disabled for i, log := range logs { if strings.EqualFold(log.PeriodsEnabled, "False") { inconsistencies = append(inconsistencies, fmt.Sprintf("periods_enabled is False in period %d", i+1)) } } if len(inconsistencies) > 0 { return true, "metadata inconsistency: " + strings.Join(inconsistencies, ", "), nil } return false, "", nil } // ValidateScoreProgression checks that scores only increase or stay the same // across periods (cumulative stats). func ValidateScoreProgression(logs []*MatchLog) (bool, string, error) { if len(logs) != 3 { return false, "", errors.New("exactly 3 period logs are required") } issues := []string{} for i := 1; i < len(logs); i++ { prev := logs[i-1] curr := logs[i] if curr.Score.Home < prev.Score.Home { issues = append(issues, fmt.Sprintf("home score decreased from period %d (%d) to period %d (%d)", i, prev.Score.Home, i+1, curr.Score.Home)) } if curr.Score.Away < prev.Score.Away { issues = append(issues, fmt.Sprintf("away score decreased from period %d (%d) to period %d (%d)", i, prev.Score.Away, i+1, curr.Score.Away)) } } if len(issues) > 0 { return true, "score regression: " + strings.Join(issues, ", "), nil } return false, "", nil } // ValidatePlayerConsistency checks that player rosters remain relatively stable // across periods. A player present in period 1 should ideally still be present // in later periods. Drastic roster changes are suspicious. func ValidatePlayerConsistency(logs []*MatchLog) (bool, string, error) { if len(logs) != 3 { return false, "", errors.New("exactly 3 period logs are required") } // Build player sets per period periodPlayers := make([]map[string]string, 3) // game_user_id -> team for i, log := range logs { periodPlayers[i] = make(map[string]string) for _, p := range log.Players { periodPlayers[i][p.GameUserID] = p.Team } } issues := []string{} // Check for team-switching between periods (same player, different team) for i := 1; i < len(logs); i++ { for id, prevTeam := range periodPlayers[i-1] { if currTeam, exists := periodPlayers[i][id]; exists { if currTeam != prevTeam { issues = append(issues, fmt.Sprintf("player %s switched teams between period %d and %d", id, i, i+1)) } } } } // Check for drastic roster changes (more than 50% different players) for i := 1; i < len(logs); i++ { prev := periodPlayers[i-1] curr := periodPlayers[i] if len(prev) == 0 { continue } missing := 0 for id := range prev { if _, exists := curr[id]; !exists { missing++ } } ratio := float64(missing) / float64(len(prev)) if ratio > 0.5 { issues = append(issues, fmt.Sprintf("more than 50%% of players changed between period %d and %d (%d/%d missing)", i, i+1, missing, len(prev))) } } if len(issues) > 0 { return true, "player inconsistency: " + strings.Join(issues, ", "), nil } return false, "", nil }