added log file uploading and match results
This commit is contained in:
180
pkg/slapshotapi/tampering.go
Normal file
180
pkg/slapshotapi/tampering.go
Normal file
@@ -0,0 +1,180 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user