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 ;
2019-06-21 15:32:47 +00:00
using System.IO ;
2018-11-28 08:19:58 +00:00
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 ;
2018-11-28 08:19:58 +00:00
using Microsoft.EntityFrameworkCore ;
2020-08-28 12:34:34 +00:00
using osu.Framework.Bindables ;
2018-11-28 10:48:15 +00:00
using osu.Framework.Logging ;
2018-11-28 07:47:10 +00:00
using osu.Framework.Platform ;
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 ;
2019-06-11 20:01:57 +00:00
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
2018-11-28 07:47:10 +00:00
using osu.Game.Rulesets ;
2021-03-18 10:19:53 +00:00
using osu.Game.Rulesets.Judgements ;
2020-08-28 10:16:46 +00:00
using osu.Game.Rulesets.Scoring ;
2018-11-28 08:19:58 +00:00
using osu.Game.Scoring.Legacy ;
2018-11-28 07:47:10 +00:00
namespace osu.Game.Scoring
{
2019-06-26 15:40:21 +00:00
public class ScoreManager : DownloadableArchiveModelManager < ScoreInfo , ScoreFileInfo >
2018-11-28 07:47:10 +00:00
{
2020-10-02 07:17:10 +00:00
public override IEnumerable < string > HandledExtensions = > new [ ] { ".osr" } ;
2018-11-28 07:47:10 +00:00
2018-11-30 08:36:06 +00:00
protected override string [ ] HashableFileTypes = > new [ ] { ".osr" } ;
2019-07-05 05:15:29 +00:00
protected override string ImportFromStablePath = > Path . Combine ( "Data" , "r" ) ;
2019-06-19 16:33:51 +00:00
2018-11-28 08:19:58 +00:00
private readonly RulesetStore rulesets ;
2019-05-09 06:15:28 +00:00
private readonly Func < BeatmapManager > beatmaps ;
2018-11-28 08:19:58 +00:00
2020-08-28 10:16:46 +00:00
[CanBeNull]
2020-11-06 04:14:23 +00:00
private readonly Func < BeatmapDifficultyCache > difficulties ;
2020-08-28 10:16:46 +00:00
2020-08-28 12:34:34 +00:00
[CanBeNull]
private readonly OsuConfigManager configManager ;
2020-08-28 10:16:46 +00:00
public ScoreManager ( RulesetStore rulesets , Func < BeatmapManager > beatmaps , Storage storage , IAPIProvider api , IDatabaseContextFactory contextFactory , IIpcHost importHost = null ,
2020-11-06 04:14:23 +00:00
Func < BeatmapDifficultyCache > difficulties = null , OsuConfigManager configManager = null )
2019-06-11 20:01:57 +00:00
: base ( storage , contextFactory , api , new ScoreStore ( contextFactory , storage ) , importHost )
2018-11-28 07:47:10 +00:00
{
2018-11-28 08:19:58 +00:00
this . rulesets = rulesets ;
this . beatmaps = beatmaps ;
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
}
2018-11-28 07:47:10 +00:00
2018-11-28 09:33:01 +00:00
protected override ScoreInfo CreateModel ( ArchiveReader archive )
2018-11-28 07:47:10 +00:00
{
2019-06-19 18:38:43 +00:00
if ( archive = = null )
2018-11-28 08:19:58 +00:00
return null ;
2020-10-16 04:21:47 +00:00
using ( var stream = archive . GetStream ( archive . Filenames . First ( f = > f . EndsWith ( ".osr" , StringComparison . OrdinalIgnoreCase ) ) ) )
2018-11-28 10:48:15 +00:00
{
try
{
2020-03-24 01:38:24 +00:00
return new DatabasedLegacyScoreDecoder ( rulesets , beatmaps ( ) ) . Parse ( stream ) . ScoreInfo ;
2018-11-28 10:48:15 +00:00
}
2020-03-24 01:38:24 +00:00
catch ( LegacyScoreDecoder . BeatmapNotFoundException e )
2018-11-28 10:48:15 +00:00
{
Logger . Log ( e . Message , LoggingTarget . Information , LogLevel . Error ) ;
return null ;
}
}
2018-11-28 07:47:10 +00:00
}
2018-11-28 08:19:58 +00:00
2021-06-27 04:06:20 +00:00
protected override Task Populate ( ScoreInfo model , ArchiveReader archive , CancellationToken cancellationToken = default )
= > Task . CompletedTask ;
2021-04-26 11:46:44 +00:00
protected override void ExportModelTo ( ScoreInfo model , Stream outputStream )
{
var file = model . Files . SingleOrDefault ( ) ;
if ( file = = null )
return ;
using ( var inputStream = Files . Storage . GetStream ( file . FileInfo . StoragePath ) )
inputStream . CopyTo ( outputStream ) ;
}
2021-02-12 03:48:32 +00:00
protected override IEnumerable < string > GetStableImportPaths ( Storage storage )
= > storage . GetFiles ( ImportFromStablePath ) . Where ( p = > HandledExtensions . Any ( ext = > Path . GetExtension ( p ) ? . Equals ( ext , StringComparison . OrdinalIgnoreCase ) ? ? false ) )
. Select ( path = > storage . GetFullPath ( path ) ) ;
2019-06-21 15:32:47 +00:00
2019-05-09 06:15:28 +00:00
public Score GetScore ( ScoreInfo score ) = > new LegacyDatabasedScore ( score , rulesets , beatmaps ( ) , Files . Store ) ;
2018-11-28 09:45:17 +00:00
public List < ScoreInfo > GetAllUsableScores ( ) = > ModelStore . ConsumableItems . Where ( s = > ! s . DeletePending ) . ToList ( ) ;
2018-11-28 08:19:58 +00:00
2018-11-29 09:30:43 +00:00
public IEnumerable < ScoreInfo > QueryScores ( Expression < Func < ScoreInfo , bool > > query ) = > ModelStore . ConsumableItems . AsNoTracking ( ) . Where ( query ) ;
2018-11-28 09:33:01 +00:00
public ScoreInfo Query ( Expression < Func < ScoreInfo , bool > > query ) = > ModelStore . ConsumableItems . AsNoTracking ( ) . FirstOrDefault ( query ) ;
2019-06-11 20:01:57 +00:00
2019-06-26 15:40:21 +00:00
protected override ArchiveDownloadRequest < ScoreInfo > CreateDownloadRequest ( ScoreInfo score , bool minimiseDownload ) = > new DownloadReplayRequest ( score ) ;
2019-06-29 05:25:30 +00:00
2019-12-17 06:34:16 +00:00
protected override bool CheckLocalAvailability ( ScoreInfo model , IQueryable < ScoreInfo > items )
= > base . CheckLocalAvailability ( model , items )
| | ( model . OnlineScoreID ! = null & & items . Any ( i = > i . OnlineScoreID = = model . OnlineScoreID ) ) ;
2020-08-28 10:16:46 +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>
public Bindable < long > GetBindableTotalScore ( ScoreInfo score )
2020-08-28 10:16:46 +00:00
{
2020-08-28 12:34:34 +00:00
var bindable = new TotalScoreBindable ( score , difficulties ) ;
configManager ? . BindWith ( OsuSetting . ScoreDisplayMode , bindable . ScoringMode ) ;
return bindable ;
}
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>
public Bindable < string > GetBindableTotalScoreString ( ScoreInfo score ) = > new TotalScoreStringBindable ( GetBindableTotalScore ( score ) ) ;
/// <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
{
public readonly Bindable < ScoringMode > ScoringMode = new Bindable < ScoringMode > ( ) ;
private readonly ScoreInfo score ;
2020-11-06 04:14:23 +00:00
private readonly Func < BeatmapDifficultyCache > difficulties ;
2020-08-28 12:34:34 +00:00
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>
2020-11-06 04:14:23 +00:00
/// <param name="difficulties">A function to retrieve the <see cref="BeatmapDifficultyCache"/>.</param>
public TotalScoreBindable ( ScoreInfo score , Func < BeatmapDifficultyCache > difficulties )
2020-08-28 10:16:46 +00:00
{
2020-08-28 12:34:34 +00:00
this . score = score ;
this . difficulties = difficulties ;
2020-08-28 10:16:46 +00:00
2020-08-28 12:34:34 +00:00
ScoringMode . BindValueChanged ( onScoringModeChanged , true ) ;
2020-08-28 10:16:46 +00:00
}
2021-02-25 07:19:01 +00:00
private IBindable < StarDifficulty ? > difficultyBindable ;
2020-09-09 08:37:11 +00:00
private CancellationTokenSource difficultyCancellationSource ;
2020-08-28 12:45:27 +00:00
2020-08-28 12:34:34 +00:00
private void onScoringModeChanged ( ValueChangedEvent < ScoringMode > mode )
{
2020-09-09 08:37:11 +00:00
difficultyCancellationSource ? . Cancel ( ) ;
difficultyCancellationSource = null ;
2020-08-28 13:51:39 +00:00
if ( score . Beatmap = = null )
{
Value = score . TotalScore ;
return ;
}
2020-09-29 09:55:06 +00:00
int beatmapMaxCombo ;
2021-03-18 10:19:53 +00:00
double accuracy = score . Accuracy ;
2020-08-28 10:16:46 +00:00
2020-09-29 09:55:06 +00:00
if ( score . IsLegacyScore )
2020-08-28 12:34:34 +00:00
{
2021-03-18 10:19:53 +00:00
if ( score . RulesetID = = 3 )
{
2021-03-18 10:26:29 +00:00
// 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.
2021-03-18 10:19:53 +00:00
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 ;
}
2020-09-29 09:55:06 +00:00
// 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 )
2020-08-28 12:34:34 +00:00
{
2020-09-29 09:55:06 +00:00
if ( score . Beatmap . ID = = 0 | | difficulties = = null )
{
// We don't have enough information (max combo) to compute the score, so use the provided score.
Value = score . TotalScore ;
return ;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
difficultyBindable = difficulties ( ) . GetBindableDifficulty ( score . Beatmap , score . Ruleset , score . Mods , ( difficultyCancellationSource = new CancellationTokenSource ( ) ) . Token ) ;
2021-02-25 07:19:01 +00:00
difficultyBindable . BindValueChanged ( d = >
{
if ( d . NewValue is StarDifficulty diff )
2021-03-18 10:19:53 +00:00
updateScore ( diff . MaxCombo , accuracy ) ;
2021-02-25 07:19:01 +00:00
} , true ) ;
2020-09-29 09:55:06 +00:00
2020-08-28 12:34:34 +00:00
return ;
}
2020-09-29 09:55:06 +00:00
beatmapMaxCombo = score . Beatmap . MaxCombo . Value ;
2020-08-28 12:34:34 +00:00
}
2020-08-28 12:45:27 +00:00
else
2020-09-29 09:55:06 +00:00
{
2021-07-04 03:39:50 +00:00
// This is guaranteed to be a non-legacy score.
2020-09-29 09:55:06 +00:00
// 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.
2021-07-18 19:52:16 +00:00
beatmapMaxCombo = Enum . GetValues ( typeof ( HitResult ) ) . OfType < HitResult > ( ) . Where ( r = > r . AffectsCombo ( ) ) . Select ( r = > score . Statistics . GetValueOrDefault ( r ) ) . Sum ( ) ;
2020-09-29 09:55:06 +00:00
}
2021-03-18 10:19:53 +00:00
updateScore ( beatmapMaxCombo , accuracy ) ;
2020-08-28 12:45:27 +00:00
}
2020-08-28 12:34:34 +00:00
2021-03-18 10:19:53 +00:00
private void updateScore ( int beatmapMaxCombo , double accuracy )
2020-08-28 12:45:27 +00:00
{
2020-08-28 13:23:44 +00:00
if ( beatmapMaxCombo = = 0 )
{
2020-08-28 13:51:19 +00:00
Value = 0 ;
2020-08-28 13:23:44 +00:00
return ;
}
2020-08-28 12:34:34 +00:00
var ruleset = score . Ruleset . CreateInstance ( ) ;
var scoreProcessor = ruleset . CreateScoreProcessor ( ) ;
2020-08-28 10:16:46 +00:00
2020-08-28 12:34:34 +00:00
scoreProcessor . Mods . Value = score . Mods ;
2021-03-18 10:19:53 +00:00
Value = ( long ) Math . Round ( scoreProcessor . GetScore ( ScoringMode . Value , beatmapMaxCombo , accuracy , ( double ) score . MaxCombo / beatmapMaxCombo , score . Statistics ) ) ;
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
}
2018-11-28 07:47:10 +00:00
}
}