181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
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
|
|
}
|