2022-07-21 02:18:57 +08: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.Diagnostics ;
using System.Linq ;
2022-09-09 15:12:18 +08:00
using System.Threading ;
2022-07-21 02:18:57 +08:00
using System.Threading.Tasks ;
2022-09-08 21:06:44 +08:00
using Newtonsoft.Json ;
2022-07-21 02:18:57 +08: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 ;
2023-06-26 21:19:01 +08:00
using osu.Game.Overlays ;
using osu.Game.Overlays.Notifications ;
2022-07-21 02:18:57 +08:00
using osu.Game.Rulesets ;
2022-09-08 21:06:44 +08:00
using osu.Game.Scoring ;
2023-06-28 14:04:13 +08:00
using osu.Game.Scoring.Legacy ;
2022-07-21 02:55:05 +08:00
using osu.Game.Screens.Play ;
2022-07-21 02:18:57 +08:00
namespace osu.Game
{
public partial class BackgroundBeatmapProcessor : Component
{
[Resolved]
private RulesetStore rulesetStore { get ; set ; } = null ! ;
2023-06-28 14:04:13 +08:00
[Resolved]
private BeatmapManager beatmapManager { get ; set ; } = null ! ;
2022-09-08 21:06:44 +08:00
[Resolved]
private ScoreManager scoreManager { get ; set ; } = null ! ;
2022-07-21 02:18:57 +08:00
[Resolved]
private RealmAccess realmAccess { get ; set ; } = null ! ;
[Resolved]
private BeatmapUpdater beatmapUpdater { get ; set ; } = null ! ;
[Resolved]
private IBindable < WorkingBeatmap > gameBeatmap { get ; set ; } = null ! ;
2022-07-21 02:55:05 +08:00
[Resolved]
private ILocalUserPlayInfo ? localUserPlayInfo { get ; set ; }
2023-06-26 21:19:01 +08:00
[Resolved]
private INotificationOverlay ? notificationOverlay { get ; set ; }
2022-07-21 17:15:21 +08:00
protected virtual int TimeToSleepDuringGameplay = > 30000 ;
2022-07-21 02:18:57 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-09-09 15:12:18 +08:00
Task . Run ( ( ) = >
2022-07-21 02:18:57 +08:00
{
Logger . Log ( "Beginning background beatmap processing.." ) ;
checkForOutdatedStarRatings ( ) ;
2022-09-09 15:12:18 +08:00
processBeatmapSetsWithMissingMetrics ( ) ;
processScoresWithMissingStatistics ( ) ;
2023-06-26 21:19:01 +08:00
convertLegacyTotalScoreToStandardised ( ) ;
2022-07-21 23:14:30 +08:00
} ) . ContinueWith ( t = >
{
if ( t . Exception ? . InnerException is ObjectDisposedException )
{
Logger . Log ( "Finished background aborted during shutdown" ) ;
return ;
}
2022-07-21 02:18:57 +08:00
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 )
{
2022-07-21 16:39:07 +08:00
b . StarRating = - 1 ;
2022-07-21 02:18:57 +08:00
countReset + + ;
}
}
r . Find < RulesetInfo > ( ruleset . ShortName ) . LastAppliedDifficultyVersion = currentVersion ;
} ) ;
Logger . Log ( $"Finished resetting {countReset} beatmap sets for {ruleset.Name}" ) ;
}
}
}
2022-09-09 15:12:18 +08:00
private void processBeatmapSetsWithMissingMetrics ( )
2022-07-21 02:18:57 +08:00
{
HashSet < Guid > beatmapSetIds = new HashSet < Guid > ( ) ;
2022-07-21 02:55:05 +08:00
Logger . Log ( "Querying for beatmap sets to reprocess..." ) ;
2022-07-21 02:18:57 +08:00
realmAccess . Run ( r = >
{
2022-07-21 16:39:07 +08:00
foreach ( var b in r . All < BeatmapInfo > ( ) . Where ( b = > b . StarRating < 0 | | ( b . OnlineID > 0 & & b . LastOnlineUpdate = = null ) ) )
2022-07-21 02:18:57 +08:00
{
Debug . Assert ( b . BeatmapSet ! = null ) ;
beatmapSetIds . Add ( b . BeatmapSet . ID ) ;
}
} ) ;
2022-07-21 02:55:05 +08:00
Logger . Log ( $"Found {beatmapSetIds.Count} beatmap sets which require reprocessing." ) ;
int i = 0 ;
2022-07-21 02:18:57 +08:00
foreach ( var id in beatmapSetIds )
{
2022-07-21 02:55:05 +08:00
while ( localUserPlayInfo ? . IsPlaying . Value = = true )
{
2022-07-21 17:15:21 +08:00
Logger . Log ( "Background processing sleeping due to active gameplay..." ) ;
2022-09-09 15:12:18 +08:00
Thread . Sleep ( TimeToSleepDuringGameplay ) ;
2022-07-21 02:55:05 +08:00
}
2022-07-21 02:18:57 +08:00
realmAccess . Run ( r = >
{
var set = r . Find < BeatmapSetInfo > ( id ) ;
if ( set ! = null )
{
2022-07-23 18:21:12 +08:00
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-21 02:18:57 +08:00
}
} ) ;
}
}
2022-09-08 21:06:44 +08:00
2022-09-09 15:12:18 +08:00
private void processScoresWithMissingStatistics ( )
2022-09-08 21:06:44 +08:00
{
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..." ) ;
2022-09-09 15:12:18 +08:00
Thread . Sleep ( TimeToSleepDuringGameplay ) ;
2022-09-08 21:06:44 +08:00
}
try
{
var score = scoreManager . Query ( s = > s . ID = = id ) ;
2022-09-09 12:57:01 +08:00
scoreManager . PopulateMaximumStatistics ( score ) ;
2022-09-08 21:06:44 +08:00
// 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}" ) ;
}
}
}
2023-06-26 21:19:01 +08:00
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 ) ;
2023-06-28 14:04:13 +08:00
long newTotalScore = StandardisedScoreMigrationTools . ConvertFromLegacyTotalScore ( score , beatmapManager ) ;
2023-06-26 21:19:01 +08:00
// 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 ;
2023-06-28 14:04:13 +08:00
s . Version = LegacyScoreEncoder . LATEST_VERSION ;
2023-06-26 21:19:01 +08:00
} ) ;
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 ;
}
}
}
2022-07-21 02:18:57 +08:00
}
}