osu/osu.Game/BackgroundDataStoreProcessor.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

387 lines
14 KiB
C#
Raw Normal View History

2022-07-20 18:18:57 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Linq;
2022-09-09 07:12:18 +00:00
using System.Threading;
2022-07-20 18:18:57 +00:00
using System.Threading.Tasks;
using Newtonsoft.Json;
2022-07-20 18:18:57 +00:00
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.Online.API;
2023-06-26 13:19:01 +00:00
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
2022-07-20 18:18:57 +00:00
using osu.Game.Rulesets;
using osu.Game.Scoring;
2023-06-28 06:04:13 +00:00
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;
2022-07-20 18:18:57 +00:00
namespace osu.Game
{
/// <summary>
/// Performs background updating of data stores at startup.
/// </summary>
public partial class BackgroundDataStoreProcessor : Component
2022-07-20 18:18:57 +00:00
{
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
2023-06-28 06:04:13 +00:00
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
2022-07-20 18:18:57 +00:00
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
private BeatmapUpdater beatmapUpdater { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
2023-06-26 13:19:01 +00:00
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
2022-07-21 09:15:21 +00:00
protected virtual int TimeToSleepDuringGameplay => 30000;
2022-07-20 18:18:57 +00:00
protected override void LoadComplete()
{
base.LoadComplete();
Task.Factory.StartNew(() =>
2022-07-20 18:18:57 +00:00
{
Logger.Log("Beginning background data store processing..");
2022-07-20 18:18:57 +00:00
checkForOutdatedStarRatings();
2022-09-09 07:12:18 +00:00
processBeatmapSetsWithMissingMetrics();
processBeatmapsWithMissingObjectCounts();
2022-09-09 07:12:18 +00:00
processScoresWithMissingStatistics();
2023-06-26 13:19:01 +00:00
convertLegacyTotalScoreToStandardised();
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
{
Logger.Log("Finished background aborted during shutdown");
return;
}
Logger.Log("Finished background data store processing!");
2022-07-20 18:18:57 +00:00
});
}
/// <summary>
/// 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.
/// </summary>
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<BeatmapInfo>())
{
if (b.Ruleset.ShortName == ruleset.ShortName)
{
b.StarRating = -1;
2022-07-20 18:18:57 +00:00
countReset++;
}
}
2023-07-06 04:37:42 +00:00
r.Find<RulesetInfo>(ruleset.ShortName)!.LastAppliedDifficultyVersion = currentVersion;
2022-07-20 18:18:57 +00:00
});
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
}
}
}
2022-09-09 07:12:18 +00:00
private void processBeatmapSetsWithMissingMetrics()
2022-07-20 18:18:57 +00:00
{
HashSet<Guid> beatmapSetIds = new HashSet<Guid>();
Logger.Log("Querying for beatmap sets to reprocess...");
2022-07-20 18:18:57 +00:00
realmAccess.Run(r =>
{
// BeatmapProcessor is responsible for both online and local processing.
// In the case a user isn't logged in, it won't update LastOnlineUpdate and therefore re-queue,
// causing overhead from the non-online processing to redundantly run every startup.
//
// We may eventually consider making the Process call more specific (or avoid this in any number
// of other possible ways), but for now avoid queueing if the user isn't logged in at startup.
if (api.IsLoggedIn)
{
foreach (var b in r.All<BeatmapInfo>().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && b.BeatmapSet != null))
beatmapSetIds.Add(b.BeatmapSet!.ID);
}
else
2022-07-20 18:18:57 +00:00
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 && b.BeatmapSet != null))
beatmapSetIds.Add(b.BeatmapSet!.ID);
2022-07-20 18:18:57 +00:00
}
});
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing.");
int i = 0;
2022-07-20 18:18:57 +00:00
foreach (var id in beatmapSetIds)
{
2023-07-04 06:35:09 +00:00
sleepIfRequired();
2022-07-20 18:18:57 +00:00
realmAccess.Run(r =>
{
var set = r.Find<BeatmapSetInfo>(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}");
}
2022-07-20 18:18:57 +00:00
}
});
}
}
private void processBeatmapsWithMissingObjectCounts()
{
Logger.Log("Querying for beatmaps with missing hitobject counts to reprocess...");
2023-12-13 08:33:24 +00:00
HashSet<Guid> beatmapIds = new HashSet<Guid>();
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount == 0))
beatmapIds.Add(b.ID);
});
Logger.Log($"Found {beatmapIds.Count} beatmaps which require reprocessing.");
int i = 0;
foreach (var id in beatmapIds)
{
sleepIfRequired();
realmAccess.Run(r =>
{
var beatmap = r.Find<BeatmapInfo>(id);
if (beatmap != null)
{
try
{
Logger.Log($"Background processing {beatmap} ({++i} / {beatmapIds.Count})");
beatmapUpdater.ProcessObjectCounts(beatmap);
}
catch (Exception e)
{
Logger.Log($"Background processing failed on {beatmap}: {e}");
}
}
});
}
}
2022-09-09 07:12:18 +00:00
private void processScoresWithMissingStatistics()
{
HashSet<Guid> scoreIds = new HashSet<Guid>();
Logger.Log("Querying for scores to reprocess...");
realmAccess.Run(r =>
{
foreach (var score in r.All<ScoreInfo>().Where(s => !s.BackgroundReprocessingFailed))
{
if (score.BeatmapInfo != null
&& 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)
{
2023-07-04 06:35:09 +00:00
sleepIfRequired();
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 =>
{
2023-07-06 04:37:42 +00:00
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
});
Logger.Log($"Populated maximum statistics for score {id}");
}
catch (ObjectDisposedException)
{
throw;
}
catch (Exception e)
{
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
}
}
}
2023-06-26 13:19:01 +00:00
private void convertLegacyTotalScoreToStandardised()
{
Logger.Log("Querying for scores that need total score conversion...");
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
&& (s.TotalScoreVersion == 30000002
|| s.TotalScoreVersion == 30000003))
.AsEnumerable().Select(s => s.ID)));
2023-06-26 13:19:01 +00:00
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
2023-07-04 06:35:09 +00:00
if (scoreIds.Count == 0)
return;
var notification = showProgressNotification("Upgrading scores to new scoring algorithm", "scores have been upgraded to the new scoring algorithm");
2023-06-26 13:19:01 +00:00
int processedCount = 0;
2023-07-04 09:34:53 +00:00
int failedCount = 0;
2023-06-26 13:19:01 +00:00
foreach (var id in scoreIds)
{
if (notification?.State == ProgressNotificationState.Cancelled)
2023-07-04 09:22:10 +00:00
break;
updateNotificationProgress(notification, processedCount, scoreIds.Count);
2023-07-04 06:35:09 +00:00
sleepIfRequired();
2023-06-26 13:19:01 +00:00
try
{
var score = scoreManager.Query(s => s.ID == id);
2023-06-28 06:04:13 +00:00
long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager);
2023-06-26 13:19:01 +00:00
// Can't use async overload because we're not on the update thread.
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
{
2023-07-06 04:37:42 +00:00
ScoreInfo s = r.Find<ScoreInfo>(id)!;
2023-06-26 13:19:01 +00:00
s.TotalScore = newTotalScore;
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
2023-06-26 13:19:01 +00:00
});
Logger.Log($"Converted total score for score {id}");
++processedCount;
2023-06-26 13:19:01 +00:00
}
catch (ObjectDisposedException)
{
throw;
}
2023-06-26 13:19:01 +00:00
catch (Exception e)
{
Logger.Log($"Failed to convert total score for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
2023-07-04 09:34:53 +00:00
++failedCount;
2023-06-26 13:19:01 +00:00
}
}
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
}
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
{
if (notification == null)
return;
notification.Text = notification.Text.ToString().Split('(').First().TrimEnd() + $" ({processedCount} of {totalCount})";
notification.Progress = (float)processedCount / totalCount;
}
private void completeNotification(ProgressNotification? notification, int processedCount, int totalCount, int? failedCount = null)
{
if (notification == null)
return;
if (processedCount == totalCount)
2023-06-26 13:19:01 +00:00
{
notification.CompletionText = $"{processedCount} {notification.CompletionText}";
notification.Progress = 1;
notification.State = ProgressNotificationState.Completed;
}
else
{
notification.Text = $"{processedCount} of {totalCount} {notification.CompletionText}";
2023-07-04 09:34:53 +00:00
// We may have arrived here due to user cancellation or completion with failures.
if (failedCount > 0)
notification.Text += $" Check logs for issues with {failedCount} failed items.";
2023-07-04 09:34:53 +00:00
notification.State = ProgressNotificationState.Cancelled;
2023-06-26 13:19:01 +00:00
}
}
2023-07-04 06:35:09 +00:00
private ProgressNotification? showProgressNotification(string running, string completed)
{
if (notificationOverlay == null)
return null;
ProgressNotification notification = new ProgressNotification
{
Text = running,
CompletionText = completed,
State = ProgressNotificationState.Active
};
notificationOverlay?.Post(notification);
return notification;
}
2023-07-04 06:35:09 +00:00
private void sleepIfRequired()
{
while (localUserPlayInfo?.IsPlaying.Value == true)
{
Logger.Log("Background processing sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay);
}
}
2022-07-20 18:18:57 +00:00
}
}