2019-01-24 16:43:03 +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.
2018-11-28 15:47:10 +08:00
2018-11-28 16:19:58 +08:00
using System ;
using System.Collections.Generic ;
2019-06-21 23:32:47 +08:00
using System.IO ;
2018-11-28 16:19:58 +08:00
using System.Linq ;
using System.Linq.Expressions ;
2020-09-09 16:37:11 +08:00
using System.Threading ;
2021-06-27 12:06:20 +08:00
using System.Threading.Tasks ;
2020-08-28 18:16:46 +08:00
using JetBrains.Annotations ;
2020-08-28 20:34:34 +08:00
using osu.Framework.Bindables ;
2018-11-28 15:47:10 +08:00
using osu.Framework.Platform ;
2021-09-01 19:56:23 +08:00
using osu.Framework.Threading ;
2018-11-28 15:47:10 +08:00
using osu.Game.Beatmaps ;
2020-08-28 20:34:34 +08:00
using osu.Game.Configuration ;
2018-11-28 15:47:10 +08:00
using osu.Game.Database ;
2021-09-30 17:21:16 +08:00
using osu.Game.IO ;
2018-11-28 15:47:10 +08:00
using osu.Game.IO.Archives ;
2019-06-12 04:01:57 +08:00
using osu.Game.Online.API ;
2021-09-30 17:21:16 +08:00
using osu.Game.Overlays.Notifications ;
2018-11-28 15:47:10 +08:00
using osu.Game.Rulesets ;
2021-03-18 18:19:53 +08:00
using osu.Game.Rulesets.Judgements ;
2020-08-28 18:16:46 +08:00
using osu.Game.Rulesets.Scoring ;
2018-11-28 15:47:10 +08:00
namespace osu.Game.Scoring
{
2021-09-30 17:21:16 +08:00
public class ScoreManager : IModelManager < ScoreInfo > , IModelFileManager < ScoreInfo , ScoreFileInfo > , IModelDownloader < ScoreInfo > , ICanAcceptFiles , IPresentImports < ScoreInfo >
2018-11-28 15:47:10 +08:00
{
2021-09-01 19:56:23 +08:00
private readonly Scheduler scheduler ;
2020-11-06 12:14:23 +08:00
private readonly Func < BeatmapDifficultyCache > difficulties ;
2020-08-28 20:34:34 +08:00
private readonly OsuConfigManager configManager ;
2021-09-30 17:21:16 +08:00
private readonly ScoreModelManager scoreModelManager ;
private readonly ScoreModelDownloader scoreModelDownloader ;
2020-08-28 20:34:34 +08:00
2021-09-01 19:56:23 +08:00
public ScoreManager ( RulesetStore rulesets , Func < BeatmapManager > beatmaps , Storage storage , IAPIProvider api , IDatabaseContextFactory contextFactory , Scheduler scheduler ,
IIpcHost importHost = null , Func < BeatmapDifficultyCache > difficulties = null , OsuConfigManager configManager = null )
2018-11-28 15:47:10 +08:00
{
2021-09-01 19:56:23 +08:00
this . scheduler = scheduler ;
2020-08-28 18:16:46 +08:00
this . difficulties = difficulties ;
2020-08-28 20:34:34 +08:00
this . configManager = configManager ;
2018-11-28 16:19:58 +08:00
2021-09-30 17:21:16 +08:00
scoreModelManager = new ScoreModelManager ( rulesets , beatmaps , storage , contextFactory , importHost ) ;
scoreModelDownloader = new ScoreModelDownloader ( scoreModelManager , api , importHost ) ;
2018-11-28 15:47:10 +08:00
}
2018-11-28 16:19:58 +08:00
2021-09-30 17:21:16 +08:00
public Score GetScore ( ScoreInfo score ) = > scoreModelManager . GetScore ( score ) ;
2021-06-27 12:06:20 +08:00
2021-09-30 17:21:16 +08:00
public List < ScoreInfo > GetAllUsableScores ( ) = > scoreModelManager . GetAllUsableScores ( ) ;
2018-11-29 17:30:43 +08:00
2021-09-30 17:21:16 +08:00
public IEnumerable < ScoreInfo > QueryScores ( Expression < Func < ScoreInfo , bool > > query ) = > scoreModelManager . QueryScores ( query ) ;
2019-06-12 04:01:57 +08:00
2021-09-30 17:21:16 +08:00
public ScoreInfo Query ( Expression < Func < ScoreInfo , bool > > query ) = > scoreModelManager . Query ( query ) ;
2020-08-28 18:16:46 +08:00
2021-09-01 14:41:52 +08: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 20:36:31 +08:00
{
var difficultyCache = difficulties ? . Invoke ( ) ;
2021-09-07 17:52:25 +08:00
if ( difficultyCache ! = null )
2021-08-31 20:36:31 +08:00
{
2021-09-07 17:52:25 +08:00
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
foreach ( var s in scores )
{
await difficultyCache . GetDifficultyAsync ( s . Beatmap , s . Ruleset , s . Mods , cancellationToken ) . ConfigureAwait ( false ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
}
2021-08-31 20:36:31 +08:00
}
2021-09-07 17:52:25 +08:00
// We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls.
return scores . OrderByDescending ( s = > GetTotalScoreAsync ( s , cancellationToken : cancellationToken ) . Result )
. ThenBy ( s = > s . OnlineScoreID )
. ToArray ( ) ;
2021-08-31 20:36:31 +08:00
}
2020-09-09 16:04:02 +08: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>
2021-09-01 19:56:23 +08:00
public Bindable < long > GetBindableTotalScore ( [ NotNull ] ScoreInfo score )
2020-08-28 18:16:46 +08:00
{
2021-08-30 18:33:09 +08:00
var bindable = new TotalScoreBindable ( score , this ) ;
2020-08-28 20:34:34 +08:00
configManager ? . BindWith ( OsuSetting . ScoreDisplayMode , bindable . ScoringMode ) ;
return bindable ;
}
2020-08-28 18:16:46 +08:00
2020-09-09 16:04:02 +08: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 19:56:23 +08:00
public Bindable < string > GetBindableTotalScoreString ( [ NotNull ] ScoreInfo score ) = > new TotalScoreStringBindable ( GetBindableTotalScore ( score ) ) ;
2020-09-09 16:04:02 +08:00
2021-09-01 14:41:52 +08:00
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
2021-09-01 19:56:23 +08:00
/// The score is returned in a callback that is run on the update thread.
2021-09-01 14:41:52 +08:00
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
2021-09-01 19:56:23 +08:00
/// <param name="callback">The callback to be invoked with the total score.</param>
2021-09-01 14:41:52 +08:00
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
2021-09-01 19:56:23 +08: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 )
. ContinueWith ( s = > scheduler . Add ( ( ) = > callback ( s . Result ) ) , TaskContinuationOptions . OnlyOnRanToCompletion ) ;
}
2021-08-30 18:33:09 +08:00
2021-09-01 14:41:52 +08: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 19:56:23 +08:00
public async Task < long > GetTotalScoreAsync ( [ NotNull ] ScoreInfo score , ScoringMode mode = ScoringMode . Standardised , CancellationToken cancellationToken = default )
2021-08-30 18:33:09 +08:00
{
if ( score . Beatmap = = null )
return score . TotalScore ;
int beatmapMaxCombo ;
double accuracy = score . Accuracy ;
if ( score . IsLegacyScore )
{
if ( score . RulesetID = = 3 )
{
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, recalculate accuracy based on the hit statistics.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
double maxBaseScore = score . Statistics . Select ( kvp = > kvp . Value ) . Sum ( ) * Judgement . ToNumericResult ( HitResult . Perfect ) ;
double baseScore = score . Statistics . Select ( kvp = > Judgement . ToNumericResult ( kvp . Key ) * kvp . Value ) . Sum ( ) ;
if ( maxBaseScore > 0 )
accuracy = baseScore / maxBaseScore ;
}
// 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.
if ( score . Beatmap . MaxCombo ! = null )
beatmapMaxCombo = score . Beatmap . MaxCombo . Value ;
else
{
if ( score . Beatmap . ID = = 0 | | difficulties = = null )
{
// We don't have enough information (max combo) to compute the score, so use the provided score.
return score . TotalScore ;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties ( ) . GetDifficultyAsync ( score . Beatmap , score . Ruleset , score . Mods , cancellationToken ) . ConfigureAwait ( false ) ;
beatmapMaxCombo = difficulty . MaxCombo ;
}
}
else
{
// 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.
beatmapMaxCombo = Enum . GetValues ( typeof ( HitResult ) ) . OfType < HitResult > ( ) . Where ( r = > r . AffectsCombo ( ) ) . Select ( r = > score . Statistics . GetValueOrDefault ( r ) ) . Sum ( ) ;
}
if ( beatmapMaxCombo = = 0 )
return 0 ;
var ruleset = score . Ruleset . CreateInstance ( ) ;
var scoreProcessor = ruleset . CreateScoreProcessor ( ) ;
scoreProcessor . Mods . Value = score . Mods ;
2021-09-01 14:41:52 +08:00
return ( long ) Math . Round ( scoreProcessor . GetScore ( mode , beatmapMaxCombo , accuracy , ( double ) score . MaxCombo / beatmapMaxCombo , score . Statistics ) ) ;
2021-08-30 18:33:09 +08:00
}
2020-09-09 16:04:02 +08: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 21:51:19 +08:00
private class TotalScoreBindable : Bindable < long >
2020-08-28 20:34:34 +08:00
{
public readonly Bindable < ScoringMode > ScoringMode = new Bindable < ScoringMode > ( ) ;
2021-09-01 19:56:23 +08:00
private readonly ScoreInfo score ;
private readonly ScoreManager scoreManager ;
private CancellationTokenSource difficultyCalculationCancellationSource ;
2020-09-09 16:04:02 +08: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 18:33:09 +08:00
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
public TotalScoreBindable ( ScoreInfo score , ScoreManager scoreManager )
2020-08-28 18:16:46 +08:00
{
2021-09-01 19:56:23 +08:00
this . score = score ;
this . scoreManager = scoreManager ;
ScoringMode . BindValueChanged ( onScoringModeChanged , true ) ;
}
private void onScoringModeChanged ( ValueChangedEvent < ScoringMode > mode )
{
difficultyCalculationCancellationSource ? . Cancel ( ) ;
difficultyCalculationCancellationSource = new CancellationTokenSource ( ) ;
scoreManager . GetTotalScore ( score , s = > Value = s , mode . NewValue , difficultyCalculationCancellationSource . Token ) ;
2020-08-28 21:51:19 +08:00
}
}
2020-09-09 16:04:02 +08: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 21:51:19 +08: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 20:34:34 +08:00
}
2020-08-28 18:16:46 +08:00
}
2021-09-30 17:21:16 +08:00
#region Implementation of IPostNotifications
public Action < Notification > PostNotification
{
set
{
scoreModelManager . PostNotification = value ;
scoreModelDownloader . PostNotification = value ;
}
}
#endregion
#region Implementation of IModelManager < ScoreInfo >
public IBindable < WeakReference < ScoreInfo > > ItemUpdated = > scoreModelManager . ItemUpdated ;
public IBindable < WeakReference < ScoreInfo > > ItemRemoved = > scoreModelManager . ItemRemoved ;
public Task ImportFromStableAsync ( StableStorage stableStorage )
{
return scoreModelManager . ImportFromStableAsync ( stableStorage ) ;
}
public void Export ( ScoreInfo item )
{
scoreModelManager . Export ( item ) ;
}
public void ExportModelTo ( ScoreInfo model , Stream outputStream )
{
scoreModelManager . ExportModelTo ( model , outputStream ) ;
}
public void Update ( ScoreInfo item )
{
scoreModelManager . Update ( item ) ;
}
public bool Delete ( ScoreInfo item )
{
return scoreModelManager . Delete ( item ) ;
}
public void Delete ( List < ScoreInfo > items , bool silent = false )
{
scoreModelManager . Delete ( items , silent ) ;
}
public void Undelete ( List < ScoreInfo > items , bool silent = false )
{
scoreModelManager . Undelete ( items , silent ) ;
}
public void Undelete ( ScoreInfo item )
{
scoreModelManager . Undelete ( item ) ;
}
public Task Import ( params string [ ] paths )
{
return scoreModelManager . Import ( paths ) ;
}
public Task Import ( params ImportTask [ ] tasks )
{
return scoreModelManager . Import ( tasks ) ;
}
public IEnumerable < string > HandledExtensions = > scoreModelManager . HandledExtensions ;
public Task < IEnumerable < ScoreInfo > > Import ( ProgressNotification notification , params ImportTask [ ] tasks )
{
return scoreModelManager . Import ( notification , tasks ) ;
}
public Task < ScoreInfo > Import ( ImportTask task , bool lowPriority = false , CancellationToken cancellationToken = default )
{
return scoreModelManager . Import ( task , lowPriority , cancellationToken ) ;
}
public Task < ScoreInfo > Import ( ArchiveReader archive , bool lowPriority = false , CancellationToken cancellationToken = default )
{
return scoreModelManager . Import ( archive , lowPriority , cancellationToken ) ;
}
public Task < ScoreInfo > Import ( ScoreInfo item , ArchiveReader archive = null , bool lowPriority = false , CancellationToken cancellationToken = default )
{
return scoreModelManager . Import ( item , archive , lowPriority , cancellationToken ) ;
}
public bool IsAvailableLocally ( ScoreInfo model )
{
return scoreModelManager . IsAvailableLocally ( model ) ;
}
#endregion
#region Implementation of IModelFileManager < in ScoreInfo , in ScoreFileInfo >
public void ReplaceFile ( ScoreInfo model , ScoreFileInfo file , Stream contents , string filename = null )
{
scoreModelManager . ReplaceFile ( model , file , contents , filename ) ;
}
public void DeleteFile ( ScoreInfo model , ScoreFileInfo file )
{
scoreModelManager . DeleteFile ( model , file ) ;
}
public void AddFile ( ScoreInfo model , Stream contents , string filename )
{
scoreModelManager . AddFile ( model , contents , filename ) ;
}
#endregion
#region Implementation of IModelDownloader < ScoreInfo >
public IBindable < WeakReference < ArchiveDownloadRequest < ScoreInfo > > > DownloadBegan = > scoreModelDownloader . DownloadBegan ;
public IBindable < WeakReference < ArchiveDownloadRequest < ScoreInfo > > > DownloadFailed = > scoreModelDownloader . DownloadFailed ;
public bool Download ( ScoreInfo model , bool minimiseDownloadSize )
{
return scoreModelDownloader . Download ( model , minimiseDownloadSize ) ;
}
public ArchiveDownloadRequest < ScoreInfo > GetExistingDownload ( ScoreInfo model )
{
return scoreModelDownloader . GetExistingDownload ( model ) ;
}
#endregion
#region Implementation of IPresentImports < ScoreInfo >
public Action < IEnumerable < ScoreInfo > > PresentImport
{
set = > scoreModelManager . PresentImport = value ;
}
#endregion
2018-11-28 15:47:10 +08:00
}
}