2019-01-24 08:43:03 +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.
2018-11-28 07:47:10 +00:00
2018-11-28 08:19:58 +00:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Linq.Expressions ;
2020-09-09 08:37:11 +00:00
using System.Threading ;
2021-06-27 04:06:20 +00:00
using System.Threading.Tasks ;
2020-08-28 10:16:46 +00:00
using JetBrains.Annotations ;
2020-08-28 12:34:34 +00:00
using osu.Framework.Bindables ;
2022-01-03 08:31:12 +00:00
using osu.Framework.Extensions ;
2018-11-28 07:47:10 +00:00
using osu.Framework.Platform ;
2021-09-01 11:56:23 +00:00
using osu.Framework.Threading ;
2018-11-28 07:47:10 +00:00
using osu.Game.Beatmaps ;
2020-08-28 12:34:34 +00:00
using osu.Game.Configuration ;
2018-11-28 07:47:10 +00:00
using osu.Game.Database ;
using osu.Game.IO.Archives ;
2021-09-30 09:21:16 +00:00
using osu.Game.Overlays.Notifications ;
2018-11-28 07:47:10 +00:00
using osu.Game.Rulesets ;
2020-08-28 10:16:46 +00:00
using osu.Game.Rulesets.Scoring ;
2018-11-28 07:47:10 +00:00
namespace osu.Game.Scoring
{
2021-11-25 08:21:05 +00:00
public class ScoreManager : IModelManager < ScoreInfo > , IModelImporter < ScoreInfo >
2018-11-28 07:47:10 +00:00
{
2022-01-24 10:59:58 +00:00
private readonly RealmAccess realm ;
2021-09-01 11:56:23 +00:00
private readonly Scheduler scheduler ;
2020-11-06 04:14:23 +00:00
private readonly Func < BeatmapDifficultyCache > difficulties ;
2020-08-28 12:34:34 +00:00
private readonly OsuConfigManager configManager ;
2021-09-30 09:21:16 +00:00
private readonly ScoreModelManager scoreModelManager ;
2020-08-28 12:34:34 +00:00
2022-01-24 10:59:58 +00:00
public ScoreManager ( RulesetStore rulesets , Func < BeatmapManager > beatmaps , Storage storage , RealmAccess realm , Scheduler scheduler ,
2022-02-01 20:20:59 +00:00
Func < BeatmapDifficultyCache > difficulties = null , OsuConfigManager configManager = null )
2018-11-28 07:47:10 +00:00
{
2022-01-24 10:59:58 +00:00
this . realm = realm ;
2021-09-01 11:56:23 +00:00
this . scheduler = scheduler ;
2020-08-28 10:16:46 +00:00
this . difficulties = difficulties ;
2020-08-28 12:34:34 +00:00
this . configManager = configManager ;
2018-11-28 08:19:58 +00:00
2022-01-24 10:59:58 +00:00
scoreModelManager = new ScoreModelManager ( rulesets , beatmaps , storage , realm ) ;
2018-11-28 07:47:10 +00:00
}
2018-11-28 08:19:58 +00:00
2021-09-30 09:21:16 +00:00
public Score GetScore ( ScoreInfo score ) = > scoreModelManager . GetScore ( score ) ;
2021-06-27 04:06:20 +00:00
2021-12-13 10:01:20 +00:00
/// <summary>
/// Perform a lookup query on available <see cref="ScoreInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
2022-01-07 15:40:14 +00:00
public ScoreInfo Query ( Expression < Func < ScoreInfo , bool > > query )
2021-12-13 10:01:20 +00:00
{
2022-01-25 04:04:05 +00:00
return realm . Run ( r = > r . All < ScoreInfo > ( ) . FirstOrDefault ( query ) ? . Detach ( ) ) ;
2021-12-13 10:01:20 +00:00
}
2020-08-28 10:16:46 +00:00
2021-09-01 06:41:52 +00:00
/// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
/// </summary>
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public async Task < ScoreInfo [ ] > OrderByTotalScoreAsync ( ScoreInfo [ ] scores , CancellationToken cancellationToken = default )
2021-08-31 12:36:31 +00:00
{
var difficultyCache = difficulties ? . Invoke ( ) ;
2021-09-07 09:52:25 +00:00
if ( difficultyCache ! = null )
2021-08-31 12:36:31 +00:00
{
2021-09-07 09:52:25 +00:00
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
foreach ( var s in scores )
{
2022-01-12 09:05:25 +00:00
await difficultyCache . GetDifficultyAsync ( s . BeatmapInfo , s . Ruleset , s . Mods , cancellationToken ) . ConfigureAwait ( false ) ;
2021-09-07 09:52:25 +00:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
}
2021-08-31 12:36:31 +00:00
}
2021-10-27 04:04:41 +00:00
long [ ] totalScores = await Task . WhenAll ( scores . Select ( s = > GetTotalScoreAsync ( s , cancellationToken : cancellationToken ) ) ) . ConfigureAwait ( false ) ;
2021-10-08 05:23:53 +00:00
2021-10-10 07:50:55 +00:00
return scores . Select ( ( score , index ) = > ( score , totalScore : totalScores [ index ] ) )
2021-10-10 06:43:24 +00:00
. OrderByDescending ( g = > g . totalScore )
2021-12-10 06:28:41 +00:00
. ThenBy ( g = > g . score . OnlineID )
2021-10-10 06:43:24 +00:00
. Select ( g = > g . score )
2021-09-07 09:52:25 +00:00
. ToArray ( ) ;
2021-08-31 12:36:31 +00:00
}
2020-09-09 08:04:02 +00:00
/// <summary>
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
/// </summary>
/// <remarks>
/// Responds to changes in the currently-selected <see cref="ScoringMode"/>.
/// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns>
2022-03-30 04:15:41 +00:00
public Bindable < long > GetBindableTotalScore ( [ NotNull ] ScoreInfo score ) = > new TotalScoreBindable ( score , this , configManager ) ;
2020-08-28 10:16:46 +00:00
2020-09-09 08:04:02 +00:00
/// <summary>
/// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>.
/// </summary>
/// <remarks>
/// Responds to changes in the currently-selected <see cref="ScoringMode"/>.
/// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the formatted total score string.</returns>
2021-09-01 11:56:23 +00:00
public Bindable < string > GetBindableTotalScoreString ( [ NotNull ] ScoreInfo score ) = > new TotalScoreStringBindable ( GetBindableTotalScore ( score ) ) ;
2020-09-09 08:04:02 +00:00
2021-09-01 06:41:52 +00:00
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
2021-09-01 11:56:23 +00:00
/// The score is returned in a callback that is run on the update thread.
2021-09-01 06:41:52 +00:00
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
2021-09-01 11:56:23 +00:00
/// <param name="callback">The callback to be invoked with the total score.</param>
2021-09-01 06:41:52 +00:00
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
2021-09-01 11:56:23 +00:00
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
public void GetTotalScore ( [ NotNull ] ScoreInfo score , [ NotNull ] Action < long > callback , ScoringMode mode = ScoringMode . Standardised , CancellationToken cancellationToken = default )
{
GetTotalScoreAsync ( score , mode , cancellationToken )
2022-03-30 04:03:10 +00:00
. ContinueWith ( task = > scheduler . Add ( ( ) = >
{
if ( ! cancellationToken . IsCancellationRequested )
callback ( task . GetResultSafely ( ) ) ;
} ) , TaskContinuationOptions . OnlyOnRanToCompletion ) ;
2021-09-01 11:56:23 +00:00
}
2021-08-30 10:33:09 +00:00
2021-09-01 06:41:52 +00:00
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The total score.</returns>
2021-09-01 11:56:23 +00:00
public async Task < long > GetTotalScoreAsync ( [ NotNull ] ScoreInfo score , ScoringMode mode = ScoringMode . Standardised , CancellationToken cancellationToken = default )
2021-08-30 10:33:09 +00:00
{
2022-01-17 04:11:43 +00:00
// TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
2022-03-08 10:07:39 +00:00
if ( string . IsNullOrEmpty ( score . BeatmapInfo . MD5Hash ) )
2022-01-17 04:11:43 +00:00
return score . TotalScore ;
2021-08-30 10:33:09 +00:00
2022-03-20 13:18:53 +00:00
int? beatmapMaxCombo = await GetMaximumAchievableComboAsync ( score , cancellationToken ) . ConfigureAwait ( false ) ;
2022-03-20 02:28:07 +00:00
if ( beatmapMaxCombo = = null )
return score . TotalScore ;
if ( beatmapMaxCombo = = 0 )
return 0 ;
2021-08-30 10:33:09 +00:00
2022-03-20 02:28:07 +00:00
var ruleset = score . Ruleset . CreateInstance ( ) ;
var scoreProcessor = ruleset . CreateScoreProcessor ( ) ;
scoreProcessor . Mods . Value = score . Mods ;
return ( long ) Math . Round ( scoreProcessor . ComputeFinalLegacyScore ( mode , score , beatmapMaxCombo . Value ) ) ;
}
/// <summary>
2022-03-20 13:18:53 +00:00
/// Retrieves the maximum achievable combo for the provided score.
2022-03-20 02:28:07 +00:00
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
2022-03-20 13:24:29 +00:00
/// <returns>The maximum achievable combo. A <see langword="null"/> return value indicates the difficulty cache has failed to retrieve the combo.</returns>
2022-03-20 13:18:53 +00:00
public async Task < int? > GetMaximumAchievableComboAsync ( [ NotNull ] ScoreInfo score , CancellationToken cancellationToken = default )
2022-03-20 02:28:07 +00:00
{
2021-08-30 10:33:09 +00:00
if ( score . IsLegacyScore )
{
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
2022-03-20 13:30:28 +00:00
#pragma warning disable CS0618
2022-01-12 09:05:25 +00:00
if ( score . BeatmapInfo . MaxCombo ! = null )
2022-03-20 02:28:07 +00:00
return score . BeatmapInfo . MaxCombo . Value ;
2022-03-20 13:30:28 +00:00
#pragma warning restore CS0618
2021-08-30 10:33:09 +00:00
2022-03-20 02:28:07 +00:00
if ( difficulties = = null )
return null ;
2021-11-20 15:54:58 +00:00
2022-03-20 02:28:07 +00:00
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties ( ) . GetDifficultyAsync ( score . BeatmapInfo , score . Ruleset , score . Mods , cancellationToken ) . ConfigureAwait ( false ) ;
return difficulty ? . MaxCombo ;
2021-08-30 10:33:09 +00:00
}
2022-03-20 02:28:07 +00:00
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
return Enum . GetValues ( typeof ( HitResult ) ) . OfType < HitResult > ( ) . Where ( r = > r . AffectsCombo ( ) ) . Select ( r = > score . Statistics . GetValueOrDefault ( r ) ) . Sum ( ) ;
2021-08-30 10:33:09 +00:00
}
2020-09-09 08:04:02 +00:00
/// <summary>
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
/// </summary>
2020-08-28 13:51:19 +00:00
private class TotalScoreBindable : Bindable < long >
2020-08-28 12:34:34 +00:00
{
2022-03-30 04:15:41 +00:00
private readonly Bindable < ScoringMode > scoringMode = new Bindable < ScoringMode > ( ) ;
2021-09-01 11:56:23 +00:00
private readonly ScoreInfo score ;
private readonly ScoreManager scoreManager ;
private CancellationTokenSource difficultyCalculationCancellationSource ;
2020-09-09 08:04:02 +00:00
/// <summary>
/// Creates a new <see cref="TotalScoreBindable"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
2021-08-30 10:33:09 +00:00
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
2022-03-30 04:15:41 +00:00
/// <param name="configManager">The config.</param>
public TotalScoreBindable ( ScoreInfo score , ScoreManager scoreManager , OsuConfigManager configManager )
2020-08-28 10:16:46 +00:00
{
2021-09-01 11:56:23 +00:00
this . score = score ;
this . scoreManager = scoreManager ;
2022-03-30 04:15:41 +00:00
configManager ? . BindWith ( OsuSetting . ScoreDisplayMode , scoringMode ) ;
scoringMode . BindValueChanged ( onScoringModeChanged , true ) ;
2021-09-01 11:56:23 +00:00
}
private void onScoringModeChanged ( ValueChangedEvent < ScoringMode > mode )
{
difficultyCalculationCancellationSource ? . Cancel ( ) ;
difficultyCalculationCancellationSource = new CancellationTokenSource ( ) ;
scoreManager . GetTotalScore ( score , s = > Value = s , mode . NewValue , difficultyCalculationCancellationSource . Token ) ;
2020-08-28 13:51:19 +00:00
}
}
2020-09-09 08:04:02 +00:00
/// <summary>
/// Provides the total score of a <see cref="ScoreInfo"/> as a formatted string. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
/// </summary>
2020-08-28 13:51:19 +00:00
private class TotalScoreStringBindable : Bindable < string >
{
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable (need to hold a reference)
private readonly IBindable < long > totalScore ;
public TotalScoreStringBindable ( IBindable < long > totalScore )
{
this . totalScore = totalScore ;
this . totalScore . BindValueChanged ( v = > Value = v . NewValue . ToString ( "N0" ) , true ) ;
2020-08-28 12:34:34 +00:00
}
2020-08-28 10:16:46 +00:00
}
2021-09-30 09:21:16 +00:00
#region Implementation of IPostNotifications
public Action < Notification > PostNotification
{
2021-11-25 08:21:05 +00:00
set = > scoreModelManager . PostNotification = value ;
2021-09-30 09:21:16 +00:00
}
#endregion
#region Implementation of IModelManager < ScoreInfo >
public bool Delete ( ScoreInfo item )
{
return scoreModelManager . Delete ( item ) ;
}
2021-12-13 10:01:20 +00:00
public void Delete ( [ CanBeNull ] Expression < Func < ScoreInfo , bool > > filter = null , bool silent = false )
{
2022-01-25 04:04:05 +00:00
realm . Run ( r = >
2021-12-13 10:01:20 +00:00
{
2022-01-25 04:04:05 +00:00
var items = r . All < ScoreInfo > ( )
. Where ( s = > ! s . DeletePending ) ;
2021-12-13 10:01:20 +00:00
if ( filter ! = null )
items = items . Where ( filter ) ;
scoreModelManager . Delete ( items . ToList ( ) , silent ) ;
2022-01-21 08:08:20 +00:00
} ) ;
2021-12-13 10:01:20 +00:00
}
2022-01-28 06:08:39 +00:00
public void Delete ( BeatmapInfo beatmap , bool silent = false )
{
realm . Run ( r = >
{
var beatmapScores = r . Find < BeatmapInfo > ( beatmap . ID ) . Scores . ToList ( ) ;
scoreModelManager . Delete ( beatmapScores , silent ) ;
} ) ;
}
2022-06-14 10:46:59 +00:00
public void Delete ( List < ScoreInfo > items , bool silent = false ) = > scoreModelManager . Delete ( items , silent ) ;
2021-09-30 09:21:16 +00:00
2022-06-14 10:46:59 +00:00
public void Undelete ( List < ScoreInfo > items , bool silent = false ) = > scoreModelManager . Undelete ( items , silent ) ;
2021-09-30 09:21:16 +00:00
2022-06-14 10:46:59 +00:00
public void Undelete ( ScoreInfo item ) = > scoreModelManager . Undelete ( item ) ;
2021-09-30 09:21:16 +00:00
2022-06-14 10:46:59 +00:00
public Task Import ( params string [ ] paths ) = > scoreModelManager . Import ( paths ) ;
2021-09-30 09:21:16 +00:00
2022-06-14 10:46:59 +00:00
public Task Import ( params ImportTask [ ] tasks ) = > scoreModelManager . Import ( tasks ) ;
2021-09-30 09:21:16 +00:00
public IEnumerable < string > HandledExtensions = > scoreModelManager . HandledExtensions ;
2022-06-14 10:46:59 +00:00
public Task < IEnumerable < Live < ScoreInfo > > > Import ( ProgressNotification notification , params ImportTask [ ] tasks ) = > scoreModelManager . Import ( notification , tasks ) ;
2021-09-30 09:21:16 +00:00
2022-06-14 10:52:30 +00:00
public Live < ScoreInfo > Import ( ScoreInfo item , ArchiveReader archive = null , bool lowPriority = false , CancellationToken cancellationToken = default ) = > scoreModelManager . Import ( item , archive , cancellationToken ) ;
2021-09-30 09:21:16 +00:00
2022-06-14 10:46:59 +00:00
public bool IsAvailableLocally ( ScoreInfo model ) = > scoreModelManager . IsAvailableLocally ( model ) ;
2021-09-30 09:21:16 +00:00
#endregion
#region Implementation of IPresentImports < ScoreInfo >
2022-01-26 04:37:33 +00:00
public Action < IEnumerable < Live < ScoreInfo > > > PostImport
2021-09-30 09:21:16 +00:00
{
2021-10-04 07:35:55 +00:00
set = > scoreModelManager . PostImport = value ;
2021-09-30 09:21:16 +00:00
}
#endregion
2018-11-28 07:47:10 +00:00
}
}