private void SaveSummary(string filename) { const int CountPerSection = 20; var results = new List <MasteryStats>(Results); results.RemoveAll(r => r.GameName.Contains("[Bonus]") || r.GameName.Contains("[Multi]") || r.GameName.EndsWith(" (Events)")); DateTime thirtyDaysAgo = DateTime.Today - TimeSpan.FromDays(30); DateTime now = DateTime.Now; var cheaters = new List <CheaterInfo>(); var cheatedGames = new List <CheatedGameInfo>(); results.Sort((l, r) => { if (l == null) { return(-1); } if (r == null) { return(1); } return(l.GameId - r.GameId); }); var detailedUserMasteryInfo = new List <string>(); Progress.Label = "Analyzing data..."; Progress.Reset(results.Count); Progress.IsEnabled = true; foreach (var result in results) { ++Progress.Current; GameStatsViewModel gameStats = null; if (!String.IsNullOrEmpty(UserMasteryDetails)) { gameStats = new GameStatsViewModel() { GameId = result.GameId }; gameStats.LoadGame(); GameStatsViewModel.UserStats userStats = null; int userIndex = -1; int masteredCount = 0; foreach (var user in gameStats.TopUsers) { if (user.PointsEarned < gameStats.TotalPoints) { break; } if (user.User == UserMasteryDetails) { userIndex = masteredCount; userStats = user; } masteredCount++; } if (userStats != null) { detailedUserMasteryInfo.Add(String.Format("{2,3}/{3,3} | {0,4}m/{1,4}m | {4,3}x {5,6}:{6}", (int)userStats.GameTime.TotalMinutes, (int)result.MeanTimeToMaster, userIndex + 1, masteredCount, gameStats.Achievements.Count(), result.GameId, result.GameName)); } } if (result.HardcoreMasteredUserCount < 8 || result.Points < 50) { continue; } var threshold = result.MeanTimeToMaster / 5; if (threshold > result.MeanTimeToMaster - result.StdDevTimeToMaster * 2) { continue; } if (gameStats == null) { gameStats = new GameStatsViewModel() { GameId = result.GameId }; gameStats.LoadGame(); } var usersToRefresh = new List <GameStatsViewModel.UserStats>(); CheatedGameInfo gameEntry = null; foreach (var user in gameStats.TopUsers) { if (user.PointsEarned == result.Points && user.GameTime.TotalMinutes < threshold) { // if the user isn't averaging at least three achievements per session, the // estimate will be off. ignore it. if (!user.IsEstimateReliable) { continue; } // some things that appear like cheating aren't. check the exceptions list. if (IgnoreCheater(user, result.GameId)) { continue; } // if the user data contains a bunch of entries without seconds, it's old. try refreshing it if (user.Achievements.Count(a => a.Value.Second == 0) > gameStats.Achievements.Count() / 2) { usersToRefresh.Add(user); } // add a new cheating entry for the user var entry = cheaters.FirstOrDefault(c => c.UserName == user.User); if (entry == null) { entry = new CheaterInfo() { UserName = user.User, Results = new List <KeyValuePair <MasteryStats, GameStatsViewModel.UserStats> >() }; cheaters.Add(entry); } entry.Results.Add(new KeyValuePair <MasteryStats, GameStatsViewModel.UserStats>(result, user)); // add a new cheating entry for the game if (gameEntry == null) { gameEntry = new CheatedGameInfo() { Game = result, Users = new List <GameStatsViewModel.UserStats>() }; cheatedGames.Add(gameEntry); } gameEntry.Users.Add(user); } } if (usersToRefresh.Count > 0) { gameStats.RefreshUsers(usersToRefresh); } } cheaters.Sort((l, r) => { int diff = (r.Results.Count - l.Results.Count); if (diff == 0) { diff = String.Compare(l.UserName, r.UserName); } return(diff); }); using (var file = File.CreateText(filename)) { file.WriteLine("Games: {0,6:D}", Snapshot.GameCount); file.WriteLine("Achievements: {0,6:D} ({1} games with achievements)", Snapshot.AchievementCount, Snapshot.AchievementGameCount); file.WriteLine("Leaderboards: {0,6:D} ({1} games with leaderboards)", Snapshot.LeaderboardCount, Snapshot.LeaderboardGameCount); file.WriteLine("RichPresences: {0,6:D} ({1} static)", Snapshot.RichPresenceCount, Snapshot.StaticRichPresenceCount); file.WriteLine("Authors: {0,6:D}", Snapshot.AuthorCount); file.WriteLine("Systems: {0,6:D}", Snapshot.SystemCount); file.WriteLine(); file.WriteLine("Most played: MAX(Players)"); file.WriteLine("```"); results.Sort((l, r) => l.NumPlayers - r.NumPlayers); for (int i = results.Count - 1, count = 0; count < CountPerSection; i--, count++) { file.WriteLine(String.Format("{0,5:D} {1}", results[i].NumPlayers, results[i].GameName)); } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Least played: MIN(Players) [Players > 0, Age > 30 days]"); file.WriteLine("```"); for (int i = 0, count = 0; count < CountPerSection || results[i].NumPlayers == results[i - 1].NumPlayers; i++) { if (results[i].NumPlayers > 0 && results[i].Created < thirtyDaysAgo) { file.WriteLine(String.Format("{0,5:D} {1}", results[i].NumPlayers, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Most Popular: MAX(Players/Day) [Age > 30 days]"); file.WriteLine("```"); results.Sort((l, r) => (int)((l.PlayersPerDay - r.PlayersPerDay) * 100000)); for (int i = results.Count - 1, count = 0; count < CountPerSection; i--) { if (results[i].NumPlayers > 0 && results[i].Created < thirtyDaysAgo) { file.WriteLine(String.Format("{0:F3} {1}", results[i].PlayersPerDay, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Least Popular: MIN(Players/Day) [Age > 30 days]"); file.WriteLine("```"); for (int i = 0, count = 0; count < CountPerSection; i++) { if (results[i].NumPlayers > 0 && results[i].Created < thirtyDaysAgo) { file.WriteLine(String.Format("{0:F4} {1}", results[i].PlayersPerDay, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Slowest to Master: MAX(MeanTimeToMaster) MasteryRate|MeanTimeToMaster|StdDev [Players Mastered >= 3]"); file.WriteLine("```"); results.Sort((l, r) => (int)((l.MeanTimeToMaster - r.MeanTimeToMaster) * 100000)); for (int i = results.Count - 1, count = 0; count < CountPerSection; i--) { if (results[i].HardcoreMasteredUserCount >= 3) { file.WriteLine(String.Format("{0,4:D}/{1,4:D} {2,8:F2} {3,8:F2} {4}", results[i].HardcoreMasteredUserCount, results[i].NumPlayers, results[i].MeanTimeToMaster, results[i].StdDevTimeToMaster, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Fastest to Master: MIN(MeanTimeToMaster) MasteryRate|MeanTimeToMaster|StdDev [Players Mastered >= 3, Points >= 50]"); file.WriteLine("```"); for (int i = 0, count = 0; count < CountPerSection; i++) { if (results[i].HardcoreMasteredUserCount >= 3 && results[i].Points >= 50 && results[i].MeanTimeToMaster > 0.0) { file.WriteLine(String.Format("{0,4:D}/{1,4:D} {2,8:F2} {3,8:F2} {4}", results[i].HardcoreMasteredUserCount, results[i].NumPlayers, results[i].MeanTimeToMaster, results[i].StdDevTimeToMaster, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Fastest to Master: MIN(MeanTimeToMaster) MasteryRate|MeanTimeToMaster|StdDev [Players Mastered >= 3, Points >= 400]"); file.WriteLine("```"); for (int i = 0, count = 0; count < CountPerSection; i++) { if (results[i].HardcoreMasteredUserCount >= 3 && results[i].Points >= 400 && results[i].MeanTimeToMaster > 0.0) { file.WriteLine(String.Format("{0,4:D}/{1,4:D} {2,8:F2} {3,8:F2} {4}", results[i].HardcoreMasteredUserCount, results[i].NumPlayers, results[i].MeanTimeToMaster, results[i].StdDevTimeToMaster, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Points requiring the least effort: MIN(MinutesPerPoint)|NintiethPercentilePoints|Players [>= 2 achievements earned by 90% of players, Players >= 3]"); file.WriteLine("```"); results.Sort((l, r) => { if (l == null) { return(-1); } if (r == null) { return(1); } return((int)((l.MinutesPerPoint - r.MinutesPerPoint) * 100000)); }); for (int i = 0, count = 0; count < CountPerSection; i++) { if (results[i].NintiethPercentileAchievements >= 3 && results[i].NumPlayers >= 3) { file.WriteLine(String.Format("{0,6:F3} {1,4:D} {2,4:D} {3}", results[i].MinutesPerPoint, results[i].NintiethPercentilePoints, results[i].NumPlayers, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Points requiring the most effort: MAX(MinutesPerPoint)|NintiethPercentilePoints|Players [>= 10 points earned by 90% of players, Players >= 3]"); file.WriteLine("```"); for (int i = results.Count - 1, count = 0; count < CountPerSection; i--) { if (results[i].NintiethPercentilePoints >= 10 && results[i].NumPlayers >= 3) { file.WriteLine(String.Format("{0,6:F3} {1,4:D} {2,4:D} {3}", results[i].MinutesPerPoint, results[i].NintiethPercentilePoints, results[i].NumPlayers, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Easiest sets: MAX(NintiethPercentilePoints/Points)|Players [Players >= 10, Points >= 50]"); file.WriteLine("```"); results.Sort((l, r) => { if (l == null || l.Points == 0) { return(-1); } if (r == null || r.Points == 0) { return(1); } return(((l.NintiethPercentilePoints * 10000) / l.Points) - ((r.NintiethPercentilePoints * 10000) / r.Points)); }); for (int i = results.Count - 1, count = 0; count < CountPerSection; i--) { if (results[i].NumPlayers >= 10 && results[i].Points >= 50) { file.WriteLine(String.Format("{0,4:D}/{1,4:D} {2,4:D} {3}", results[i].NintiethPercentilePoints, results[i].Points, results[i].NumPlayers, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Easiest sets: MAX(NintiethPercentilePoints/Points)|Players [Players >= 10, Points >= 400]"); file.WriteLine("```"); for (int i = results.Count - 1, count = 0; count < CountPerSection; i--) { if (results[i].NumPlayers >= 10 && results[i].Points >= 400) { file.WriteLine(String.Format("{0,4:D}/{1,4:D} {2,4:D} {3}", results[i].NintiethPercentilePoints, results[i].Points, results[i].NumPlayers, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Hardest sets: MIN(TwentyFifthPercentilePoints/Points)|Players [Players >= 10, TwentyFifthPercentilePoints > 0]"); file.WriteLine("```"); results.Sort((l, r) => { if (l == null || l.Points == 0) { return(-1); } if (r == null || r.Points == 0) { return(1); } return((l.TwentyFifthPercentilePoints * 10000) / l.Points - (r.TwentyFifthPercentilePoints * 10000) / r.Points); }); for (int i = 0, count = 0; count < CountPerSection; i++) { if (results[i].NumPlayers >= 10 && results[i].TwentyFifthPercentilePoints > 0) { file.WriteLine(String.Format("{0,4:D}/{1,4:D} {2,4:D} {3}", results[i].TwentyFifthPercentilePoints, results[i].Points, results[i].NumPlayers, results[i].GameName)); count++; } } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Most Earned Achievements: MAX(Players)|Achievement (Game)"); file.WriteLine("```"); foreach (var achievement in _mostAwardedAchievements) { file.WriteLine(String.Format("{0,5:D} {1} ({2})", achievement.EarnedBy, achievement.Title, achievement.Description)); } file.WriteLine("```"); file.WriteLine(); file.WriteLine("Possible cheaters: TimeToMaster < 20% of median Time/Median/StdDev|LinkToComparePage [Masters >= 8, Points >= 50, TimeToMaster more than 2 stddevs from median]"); file.WriteLine(); foreach (var cheater in cheaters) { foreach (var kvp in cheater.Results) { var result = kvp.Key; var user = kvp.Value; file.WriteLine("* {0} mastered {1} ({2})", user.User, result.GameName, result.GameId); file.WriteLine(" https://retroachievements.org/gamecompare.php?ID={0}&f={1}", result.GameId, user.User); bool dumpTimes = true; var notified = CheaterNotified(user.User, result.GameId); if (!String.IsNullOrEmpty(notified)) { file.WriteLine(" - notified {0}", notified); } if (IsUntracked(user.User)) { file.WriteLine(" - currently Untracked"); dumpTimes = false; } var performance = 1.0 - (user.GameTime.TotalMinutes / result.MeanTimeToMaster); file.WriteLine(" Time to Master: {0:F2} ({1:F2}% faster than median {2:F2}, std dev={3:F2})", user.GameTime.TotalMinutes, performance * 100, result.MeanTimeToMaster, result.StdDevTimeToMaster); if (dumpTimes) { var achievements = new List <AchievementTime>(); foreach (var achievement in user.Achievements) { achievements.Add(new AchievementTime { Id = achievement.Key, When = achievement.Value }); } achievements.Sort((l, r) => DateTime.Compare(l.When, r.When)); var gameStats = new GameStatsViewModel() { GameId = result.GameId }; gameStats.LoadGame(); foreach (var achievement in achievements) { file.Write(" {0:D4}-{1:D2}-{2:D2} {3:D2}:{4:D2}:{5:D2} ", achievement.When.Year, achievement.When.Month, achievement.When.Day, achievement.When.Hour, achievement.When.Minute, achievement.When.Second); var achDef = gameStats.Achievements.FirstOrDefault(a => a.Id == achievement.Id); if (achDef != null) { file.WriteLine("{0,6:D} {1}", achDef.Id, achDef.Title); } else { file.WriteLine("{0,6:D} ??????", achievement.Id); } } } file.WriteLine(); } } file.WriteLine(); file.WriteLine("Most cheated games: Count|ID|Name [most frequent games from previous list]"); file.WriteLine("```"); cheatedGames.Sort((l, r) => { int diff = (r.Users.Count - l.Users.Count); if (diff == 0) { diff = String.Compare(l.Game.GameName, r.Game.GameName); } return(diff); }); for (int i = 0, count = 0; (count < CountPerSection && cheatedGames[i].Users.Count > 1) || (i > 0 && cheatedGames[i].Users.Count == cheatedGames[i - 1].Users.Count); i++) { file.WriteLine(String.Format("{0,2:D} {1,5:D} {2}", cheatedGames[i].Users.Count, cheatedGames[i].Game.GameId, cheatedGames[i].Game.GameName)); } file.WriteLine("```"); file.WriteLine(); if (!String.IsNullOrEmpty(UserMasteryDetails)) { file.Write("Details for "); file.WriteLine(UserMasteryDetails); file.WriteLine(" rank | mastery | achs gameid:name"); file.WriteLine(" ------ | ----------- | -----------------------------------------------------------------------"); detailedUserMasteryInfo.Sort(); foreach (var line in detailedUserMasteryInfo) { file.WriteLine(line); } } } Progress.Label = String.Empty; }
private static bool IgnoreCheater(GameStatsViewModel.UserStats user, int gameId) { switch (user.User) { case "AgentRibinski": // seems like a bad estimation caused by playing over many days return(gameId == 669); case "Agrahnax": // seems like a bad estimation caused by playing over many days return(gameId == 676); case "amine456": // televandalist seems to think the times seem reasonable // https://discord.com/channels/310192285306454017/564271682731245603/794743956948647936 return(gameId == 535); case "Baobabastass": // KickMeElmo indicates the game has bugs which allow endgame equipment early // https://discord.com/channels/310192285306454017/564271682731245603/794680166306676766 return(gameId == 762); case "boxmeister": // suspicious achievements were all added after the bulk of the achievements were unlocked. // suspect he completed the game normally, then unlocked the new achievements with his existing save return(gameId == 802); case "chro": // two rapid sessions with a big gap in the middle // https://discord.com/channels/310192285306454017/564271682731245603/794635369555034142 return(gameId == 788); case "joker1000": // golden sun was broken // https://discord.com/channels/310192285306454017/564271682731245603/826985751379050566 if (gameId == 2592) { return(true); } break; case "Nevermond12": // he's just that good // https://discord.com/channels/310192285306454017/564271682731245603/826985306074120202 return(gameId == 10173); case "Riger": // Salsa manually unlocked a bunch of stuff for Riger on 4/14/2018: https://discord.com/channels/310192285306454017/360584144281010178/434742993728307201 if (user.Achievements.First().Value.Year == 2018) { return(true); } break; case "Valenstein": // seems like a bad estimation caused by playing over many days return(gameId == 782); case "VICTORKRATOS": // likely offline/reconnect unlocks // https://discord.com/channels/310192285306454017/564271682731245603/794620459374477362 return(gameId == 624 || gameId == 1485); } return(false); }