// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game { public partial class BackgroundBeatmapProcessor : Component { [Resolved] private RulesetStore rulesetStore { get; set; } = null!; [Resolved] private ScoreManager scoreManager { get; set; } = null!; [Resolved] private RealmAccess realmAccess { get; set; } = null!; [Resolved] private BeatmapUpdater beatmapUpdater { get; set; } = null!; [Resolved] private IBindable gameBeatmap { get; set; } = null!; [Resolved] private ILocalUserPlayInfo? localUserPlayInfo { get; set; } [Resolved] private INotificationOverlay? notificationOverlay { get; set; } protected virtual int TimeToSleepDuringGameplay => 30000; protected override void LoadComplete() { base.LoadComplete(); Task.Run(() => { Logger.Log("Beginning background beatmap processing.."); checkForOutdatedStarRatings(); processBeatmapSetsWithMissingMetrics(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); }).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) { Logger.Log("Finished background aborted during shutdown"); return; } Logger.Log("Finished background beatmap processing!"); }); } /// /// Check whether the databased difficulty calculation version matches the latest ruleset provided version. /// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated. /// private void checkForOutdatedStarRatings() { foreach (var ruleset in rulesetStore.AvailableRulesets) { // beatmap being passed in is arbitrary here. just needs to be non-null. int currentVersion = ruleset.CreateInstance().CreateDifficultyCalculator(gameBeatmap.Value).Version; if (ruleset.LastAppliedDifficultyVersion < currentVersion) { Logger.Log($"Resetting star ratings for {ruleset.Name} (difficulty calculation version updated from {ruleset.LastAppliedDifficultyVersion} to {currentVersion})"); int countReset = 0; realmAccess.Write(r => { foreach (var b in r.All()) { if (b.Ruleset.ShortName == ruleset.ShortName) { b.StarRating = -1; countReset++; } } r.Find(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion; }); Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}"); } } } private void processBeatmapSetsWithMissingMetrics() { HashSet beatmapSetIds = new HashSet(); Logger.Log("Querying for beatmap sets to reprocess..."); realmAccess.Run(r => { foreach (var b in r.All().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null))) { Debug.Assert(b.BeatmapSet != null); beatmapSetIds.Add(b.BeatmapSet.ID); } }); Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); int i = 0; foreach (var id in beatmapSetIds) { while (localUserPlayInfo?.IsPlaying.Value == true) { Logger.Log("Background processing sleeping due to active gameplay..."); Thread.Sleep(TimeToSleepDuringGameplay); } realmAccess.Run(r => { var set = r.Find(id); if (set != null) { try { Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})"); beatmapUpdater.Process(set); } catch (Exception e) { Logger.Log($"Background processing failed on {set}: {e}"); } } }); } } private void processScoresWithMissingStatistics() { HashSet scoreIds = new HashSet(); Logger.Log("Querying for scores to reprocess..."); realmAccess.Run(r => { foreach (var score in r.All()) { if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0) scoreIds.Add(score.ID); } }); Logger.Log($"Found {scoreIds.Count} scores which require reprocessing."); foreach (var id in scoreIds) { while (localUserPlayInfo?.IsPlaying.Value == true) { Logger.Log("Background processing sleeping due to active gameplay..."); Thread.Sleep(TimeToSleepDuringGameplay); } try { var score = scoreManager.Query(s => s.ID == id); scoreManager.PopulateMaximumStatistics(score); // Can't use async overload because we're not on the update thread. // ReSharper disable once MethodHasAsyncOverload realmAccess.Write(r => { r.Find(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); }); Logger.Log($"Populated maximum statistics for score {id}"); } catch (Exception e) { Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); } } } private void convertLegacyTotalScoreToStandardised() { HashSet scoreIds = new HashSet(); Logger.Log("Querying for scores that need total score conversion..."); realmAccess.Run(r => { foreach (var score in r.All().Where(s => s.IsLegacyScore)) { if (score.RulesetID is not (0 or 1 or 2 or 3)) continue; if (score.Version >= 30000003) continue; scoreIds.Add(score.ID); } }); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); ProgressNotification? notification = null; if (scoreIds.Count > 0) notificationOverlay?.Post(notification = new ProgressNotification { State = ProgressNotificationState.Active }); int count = 0; updateNotification(); foreach (var id in scoreIds) { while (localUserPlayInfo?.IsPlaying.Value == true) { Logger.Log("Background processing sleeping due to active gameplay..."); Thread.Sleep(TimeToSleepDuringGameplay); } try { var score = scoreManager.Query(s => s.ID == id); long newTotalScore = scoreManager.ConvertFromLegacyTotalScore(score); // Can't use async overload because we're not on the update thread. // ReSharper disable once MethodHasAsyncOverload realmAccess.Write(r => { ScoreInfo s = r.Find(id); s.TotalScore = newTotalScore; s.Version = 30000003; }); Logger.Log($"Converted total score for score {id}"); } catch (Exception e) { Logger.Log($"Failed to convert total score for {id}: {e}"); } ++count; updateNotification(); } void updateNotification() { if (notification == null) return; if (count == scoreIds.Count) { notification.CompletionText = $"Total score updated for {scoreIds.Count} scores"; notification.Progress = 1; notification.State = ProgressNotificationState.Completed; } else { notification.Text = $"Total score updated for {count} of {scoreIds.Count} scores"; notification.Progress = (float)count / scoreIds.Count; } } } } }