1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-18 11:03:22 +08:00
osu-lazer/osu.Game/BackgroundBeatmapProcessor.cs
2023-06-26 22:19:01 +09:00

286 lines
9.9 KiB
C#

// 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.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<WorkingBeatmap> 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!");
});
}
/// <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;
countReset++;
}
}
r.Find<RulesetInfo>(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion;
});
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
}
}
}
private void processBeatmapSetsWithMissingMetrics()
{
HashSet<Guid> beatmapSetIds = new HashSet<Guid>();
Logger.Log("Querying for beatmap sets to reprocess...");
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().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<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}");
}
}
});
}
}
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>())
{
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<ScoreInfo>(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<Guid> scoreIds = new HashSet<Guid>();
Logger.Log("Querying for scores that need total score conversion...");
realmAccess.Run(r =>
{
foreach (var score in r.All<ScoreInfo>().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<ScoreInfo>(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;
}
}
}
}
}