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-04-13 17:19:50 +08:00
2017-03-15 13:06:05 +08:00
using System ;
2017-03-31 14:59:53 +08:00
using System.Collections.Generic ;
2021-12-06 14:35:08 +08:00
using System.Linq ;
2022-01-10 12:25:23 +08:00
using JetBrains.Annotations ;
2021-12-06 14:35:08 +08:00
using Newtonsoft.Json ;
2021-12-06 21:47:00 +08:00
using osu.Framework.Localisation ;
2017-07-26 12:22:46 +08:00
using osu.Game.Beatmaps ;
2018-11-28 15:39:08 +08:00
using osu.Game.Database ;
2021-12-06 14:31:40 +08:00
using osu.Game.Models ;
2021-12-06 14:35:08 +08:00
using osu.Game.Online.API ;
2021-12-13 16:37:27 +08:00
using osu.Game.Online.API.Requests.Responses ;
2018-11-28 15:33:42 +08:00
using osu.Game.Rulesets ;
2021-12-06 14:35:08 +08:00
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Scoring ;
2023-06-26 16:52:47 +08:00
using osu.Game.Scoring.Legacy ;
2021-11-11 22:18:31 +08:00
using osu.Game.Users ;
2021-12-06 21:47:00 +08:00
using osu.Game.Utils ;
2021-12-06 14:31:40 +08:00
using Realms ;
2018-11-28 15:12:57 +08:00
namespace osu.Game.Scoring
2016-11-29 14:41:48 +08:00
{
2023-02-08 10:40:20 +08:00
/// <summary>
2023-02-08 13:20:58 +08:00
/// A realm model containing metadata for a single score.
2023-02-08 10:50:52 +08:00
/// </summary>
2021-12-06 14:31:40 +08:00
[MapTo("Score")]
public class ScoreInfo : RealmObject , IHasGuidPrimaryKey , IHasRealmFiles , ISoftDelete , IEquatable < ScoreInfo > , IScoreInfo
2016-11-29 14:41:48 +08:00
{
2021-12-06 14:31:40 +08:00
[PrimaryKey]
2022-01-20 15:43:51 +08:00
public Guid ID { get ; set ; }
2018-04-13 17:19:50 +08:00
2023-02-08 13:20:58 +08:00
/// <summary>
/// The <see cref="BeatmapInfo"/> this score was made against.
/// </summary>
/// <remarks>
2023-07-05 04:20:50 +08:00
/// <para>
/// This property may be <see langword="null"/> if the score was set on a beatmap (or a version of the beatmap) that is not available locally
/// e.g. due to online updates, or local modifications to the beatmap.
/// The property will only link to a <see cref="BeatmapInfo"/> if its <see cref="Beatmaps.BeatmapInfo.Hash"/> matches <see cref="BeatmapHash"/>.
/// </para>
/// <para>
/// Due to the above, whenever setting this, make sure to also set <see cref="BeatmapHash"/> to allow relational consistency when a beatmap is potentially changed.
/// </para>
2023-02-08 13:20:58 +08:00
/// </remarks>
2023-07-04 13:50:34 +08:00
public BeatmapInfo ? BeatmapInfo { get ; set ; }
2022-01-17 12:51:30 +08:00
2023-12-21 17:14:24 +08:00
/// <summary>
/// The version of the client this score was set using.
/// Sourced from <see cref="OsuGameBase.Version"/> at the point of score submission.
/// </summary>
2023-12-21 19:56:19 +08:00
public string ClientVersion { get ; set ; } = string . Empty ;
2023-12-21 17:14:24 +08:00
2023-02-08 13:20:58 +08:00
/// <summary>
2023-02-08 14:39:18 +08:00
/// The <see cref="osu.Game.Beatmaps.BeatmapInfo.Hash"/> at the point in time when the score was set.
2023-02-08 13:20:58 +08:00
/// </summary>
public string BeatmapHash { get ; set ; } = string . Empty ;
2022-01-20 15:43:51 +08:00
public RulesetInfo Ruleset { get ; set ; } = null ! ;
2022-01-17 12:51:30 +08:00
2021-12-06 14:31:40 +08:00
public IList < RealmNamedFileUsage > Files { get ; } = null ! ;
2021-04-21 14:16:28 +08:00
2021-12-06 14:31:40 +08:00
public string Hash { get ; set ; } = string . Empty ;
2018-11-30 16:36:06 +08:00
2018-11-28 15:39:08 +08:00
public bool DeletePending { get ; set ; }
2018-11-30 15:11:09 +08:00
2024-04-17 14:51:53 +08:00
/// <summary>
/// The total number of points awarded for the score.
/// </summary>
2022-01-17 12:51:30 +08:00
public long TotalScore { get ; set ; }
2024-04-17 14:51:53 +08:00
/// <summary>
/// The total number of points awarded for the score without including mod multipliers.
/// </summary>
/// <remarks>
/// The purpose of this property is to enable future lossless rebalances of mod multipliers.
/// </remarks>
public long TotalScoreWithoutMods { get ; set ; }
2023-07-04 19:02:25 +08:00
/// <summary>
/// The version of processing applied to calculate total score as stored in the database.
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
/// the total score has not yet been updated to reflect the current scoring values.
///
2023-07-26 15:07:45 +08:00
/// See <see cref="BackgroundDataStoreProcessor"/>'s conversion logic.
2023-07-04 19:02:25 +08:00
/// </summary>
/// <remarks>
/// This may not match the version stored in the replay files.
/// </remarks>
2023-07-05 18:47:44 +08:00
public int TotalScoreVersion { get ; set ; } = LegacyScoreEncoder . LATEST_VERSION ;
2023-07-04 19:02:25 +08:00
2023-06-27 16:18:32 +08:00
/// <summary>
/// Used to preserve the total score for legacy scores.
/// </summary>
2023-06-28 14:04:13 +08:00
/// <remarks>
/// Not populated if <see cref="IsLegacyScore"/> is <c>false</c>.
/// </remarks>
2023-07-05 18:47:44 +08:00
public long? LegacyTotalScore { get ; set ; }
2023-06-27 13:59:40 +08:00
2023-07-26 15:08:02 +08:00
/// <summary>
2023-08-21 18:36:22 +08:00
/// If background processing of this beatmap failed in some way, this flag will become <c>true</c>.
/// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail.
2023-07-26 15:08:02 +08:00
/// </summary>
/// <remarks>
2023-08-21 18:35:04 +08:00
/// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk).
2023-07-26 15:08:02 +08:00
/// </remarks>
2023-08-21 18:36:22 +08:00
public bool BackgroundReprocessingFailed { get ; set ; }
2023-07-26 15:08:02 +08:00
2022-01-17 12:51:30 +08:00
public int MaxCombo { get ; set ; }
public double Accuracy { get ; set ; }
2023-09-07 17:26:51 +08:00
[Ignored]
public bool HasOnlineReplay { get ; set ; }
2022-01-17 12:51:30 +08:00
public DateTimeOffset Date { get ; set ; }
public double? PP { get ; set ; }
2020-09-29 17:55:06 +08:00
2024-02-06 04:27:16 +08:00
/// <summary>
/// Whether the performance points in this score is awarded to the player. This is used for online display purposes (see <see cref="SoloScoreInfo.Ranked"/>).
/// </summary>
[Ignored]
public bool Ranked { get ; set ; }
2023-09-01 13:47:07 +08:00
/// <summary>
/// The online ID of this score.
/// </summary>
/// <remarks>
/// In the osu-web database, this ID (if present) comes from the new <c>solo_scores</c> table.
/// </remarks>
2021-12-06 14:31:40 +08:00
[Indexed]
public long OnlineID { get ; set ; } = - 1 ;
2020-09-25 19:22:59 +08:00
2023-09-01 13:47:07 +08:00
/// <summary>
/// The legacy online ID of this score.
/// </summary>
/// <remarks>
/// In the osu-web database, this ID (if present) comes from the legacy <c>osu_scores_*_high</c> tables.
/// This ID is also stored to replays set on osu!stable.
/// </remarks>
[Indexed]
public long LegacyOnlineID { get ; set ; } = - 1 ;
2021-12-06 21:47:00 +08:00
[MapTo("User")]
2022-01-20 15:43:51 +08:00
public RealmUser RealmUser { get ; set ; } = null ! ;
2021-12-06 21:47:00 +08:00
2022-01-13 11:59:16 +08:00
[MapTo("Mods")]
public string ModsJson { get ; set ; } = string . Empty ;
[MapTo("Statistics")]
public string StatisticsJson { get ; set ; } = string . Empty ;
2022-08-22 16:42:41 +08:00
[MapTo("MaximumStatistics")]
public string MaximumStatisticsJson { get ; set ; } = string . Empty ;
2022-01-20 15:43:51 +08:00
public ScoreInfo ( BeatmapInfo ? beatmap = null , RulesetInfo ? ruleset = null , RealmUser ? realmUser = null )
2022-01-10 12:25:23 +08:00
{
2022-01-20 15:43:51 +08:00
Ruleset = ruleset ? ? new RulesetInfo ( ) ;
BeatmapInfo = beatmap ? ? new BeatmapInfo ( ) ;
2023-07-01 14:49:06 +08:00
BeatmapHash = BeatmapInfo . Hash ;
2022-01-20 15:43:51 +08:00
RealmUser = realmUser ? ? new RealmUser ( ) ;
ID = Guid . NewGuid ( ) ;
2022-01-10 12:25:23 +08:00
}
2022-01-20 15:43:51 +08:00
[UsedImplicitly] // Realm
private ScoreInfo ( )
2022-01-10 12:25:23 +08:00
{
}
2021-12-13 16:37:27 +08:00
// TODO: this is a bit temporary to account for the fact that this class is used to ferry API user data to certain UI components.
// Eventually we should either persist enough information to realm to not require the API lookups, or perform the API lookups locally.
private APIUser ? user ;
2022-01-18 14:21:08 +08:00
[Ignored]
2021-12-13 16:37:27 +08:00
public APIUser User
2021-12-06 21:47:00 +08:00
{
2021-12-13 16:37:27 +08:00
get = > user ? ? = new APIUser
2021-12-06 21:47:00 +08:00
{
2021-12-13 16:37:27 +08:00
Id = RealmUser . OnlineID ,
2022-07-16 11:30:25 +08:00
Username = RealmUser . Username ,
2022-07-18 15:16:59 +08:00
CountryCode = RealmUser . CountryCode ,
2021-12-06 21:47:00 +08:00
} ;
2021-12-13 16:37:27 +08:00
set
{
user = value ;
RealmUser = new RealmUser
{
OnlineID = user . OnlineID ,
2022-07-16 11:30:25 +08:00
Username = user . Username ,
2022-07-18 15:16:59 +08:00
CountryCode = user . CountryCode ,
2021-12-13 16:37:27 +08:00
} ;
}
2021-12-06 21:47:00 +08:00
}
2020-09-25 19:22:59 +08:00
2022-01-19 08:46:45 +08:00
[Ignored]
2021-12-06 14:31:40 +08:00
public ScoreRank Rank
2019-12-03 12:33:42 +08:00
{
2021-12-06 14:31:40 +08:00
get = > ( ScoreRank ) RankInt ;
set = > RankInt = ( int ) value ;
2019-12-03 12:33:42 +08:00
}
2021-10-29 10:48:36 +08:00
2021-12-06 14:31:40 +08:00
[MapTo(nameof(Rank))]
public int RankInt { get ; set ; }
2021-10-28 17:23:52 +08:00
IRulesetInfo IScoreInfo . Ruleset = > Ruleset ;
2023-07-04 13:50:34 +08:00
IBeatmapInfo ? IScoreInfo . Beatmap = > BeatmapInfo ;
2021-11-11 22:18:31 +08:00
IUser IScoreInfo . User = > User ;
2021-12-06 14:35:08 +08:00
#region Properties required to make things work with existing usages
2021-12-06 21:47:00 +08:00
public int UserID = > RealmUser . OnlineID ;
public int RulesetID = > Ruleset . OnlineID ;
2021-12-06 14:35:08 +08:00
[Ignored]
public List < HitEvent > HitEvents { get ; set ; } = new List < HitEvent > ( ) ;
public ScoreInfo DeepClone ( )
{
2022-01-14 12:08:20 +08:00
var clone = ( ScoreInfo ) this . Detach ( ) . MemberwiseClone ( ) ;
2021-12-06 14:35:08 +08:00
clone . Statistics = new Dictionary < HitResult , int > ( clone . Statistics ) ;
2022-08-25 12:58:57 +08:00
clone . MaximumStatistics = new Dictionary < HitResult , int > ( clone . MaximumStatistics ) ;
2023-12-28 01:38:29 +08:00
clone . HitEvents = new List < HitEvent > ( clone . HitEvents ) ;
2022-09-18 22:48:03 +08:00
// Ensure we have fresh mods to avoid any references (ie. after gameplay).
clone . clearAllMods ( ) ;
clone . ModsJson = ModsJson ;
2022-01-26 13:25:55 +08:00
clone . RealmUser = new RealmUser
{
OnlineID = RealmUser . OnlineID ,
Username = RealmUser . Username ,
2022-07-18 15:16:59 +08:00
CountryCode = RealmUser . CountryCode ,
2022-01-26 13:25:55 +08:00
} ;
2021-12-06 14:35:08 +08:00
return clone ;
}
[Ignored]
public bool Passed { get ; set ; } = true ;
2021-12-06 21:47:00 +08:00
public int Combo { get ; set ; }
2021-12-06 14:35:08 +08:00
/// <summary>
/// The position of this score, starting at 1.
/// </summary>
[Ignored]
public int? Position { get ; set ; } // TODO: remove after all calls to `CreateScoreInfo` are gone.
2021-12-06 21:47:00 +08:00
[Ignored]
public LocalisableString DisplayAccuracy = > Accuracy . FormatAccuracy ( ) ;
2021-12-06 14:35:08 +08:00
/// <summary>
2022-03-20 21:30:28 +08:00
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
2021-12-06 14:35:08 +08:00
/// </summary>
2023-06-08 20:24:40 +08:00
public bool IsLegacyScore { get ; set ; }
2021-12-06 14:35:08 +08:00
2022-01-13 11:59:16 +08:00
private Dictionary < HitResult , int > ? statistics ;
2022-01-10 13:18:34 +08:00
2021-12-06 14:35:08 +08:00
[Ignored]
2022-01-13 11:59:16 +08:00
public Dictionary < HitResult , int > Statistics
2021-12-06 14:35:08 +08:00
{
get
{
2022-01-13 11:59:16 +08:00
if ( statistics ! = null )
return statistics ;
2021-12-06 14:35:08 +08:00
2022-01-13 11:59:16 +08:00
if ( ! string . IsNullOrEmpty ( StatisticsJson ) )
statistics = JsonConvert . DeserializeObject < Dictionary < HitResult , int > > ( StatisticsJson ) ;
return statistics ? ? = new Dictionary < HitResult , int > ( ) ;
}
set = > statistics = value ;
}
2022-08-22 16:42:41 +08:00
private Dictionary < HitResult , int > ? maximumStatistics ;
[Ignored]
public Dictionary < HitResult , int > MaximumStatistics
{
get
{
if ( maximumStatistics ! = null )
return maximumStatistics ;
if ( ! string . IsNullOrEmpty ( MaximumStatisticsJson ) )
maximumStatistics = JsonConvert . DeserializeObject < Dictionary < HitResult , int > > ( MaximumStatisticsJson ) ;
return maximumStatistics ? ? = new Dictionary < HitResult , int > ( ) ;
}
set = > maximumStatistics = value ;
}
2022-01-13 11:59:16 +08:00
private Mod [ ] ? mods ;
[Ignored]
public Mod [ ] Mods
{
get
{
2021-12-06 14:35:08 +08:00
if ( mods ! = null )
2022-01-13 11:59:16 +08:00
return mods ;
2021-12-06 14:35:08 +08:00
2022-01-18 14:46:27 +08:00
return APIMods . Select ( m = > m . ToMod ( Ruleset . CreateInstance ( ) ) ) . ToArray ( ) ;
2021-12-06 14:35:08 +08:00
}
set
{
2022-01-19 13:17:56 +08:00
clearAllMods ( ) ;
2021-12-06 14:35:08 +08:00
mods = value ;
2022-01-13 11:59:16 +08:00
updateModsJson ( ) ;
2021-12-06 14:35:08 +08:00
}
}
2022-01-13 11:59:16 +08:00
private APIMod [ ] ? apiMods ;
2021-12-06 14:35:08 +08:00
// Used for API serialisation/deserialisation.
[Ignored]
public APIMod [ ] APIMods
{
get
{
2022-01-13 11:59:16 +08:00
if ( apiMods ! = null ) return apiMods ;
2021-12-06 14:35:08 +08:00
2022-01-13 11:59:16 +08:00
// prioritise reading from realm backing
if ( ! string . IsNullOrEmpty ( ModsJson ) )
apiMods = JsonConvert . DeserializeObject < APIMod [ ] > ( ModsJson ) ;
2021-12-06 14:35:08 +08:00
2022-01-13 11:59:16 +08:00
// then check mods set via Mods property.
if ( mods ! = null )
2022-01-18 14:46:27 +08:00
apiMods ? ? = mods . Select ( m = > new APIMod ( m ) ) . ToArray ( ) ;
2022-01-13 11:59:16 +08:00
return apiMods ? ? Array . Empty < APIMod > ( ) ;
2021-12-06 14:35:08 +08:00
}
set
{
2022-01-19 13:17:56 +08:00
clearAllMods ( ) ;
2022-01-13 11:59:16 +08:00
apiMods = value ;
updateModsJson ( ) ;
2021-12-06 14:35:08 +08:00
}
}
2022-01-19 13:17:56 +08:00
private void clearAllMods ( )
{
ModsJson = string . Empty ;
mods = null ;
apiMods = null ;
}
2022-01-13 11:59:16 +08:00
private void updateModsJson ( )
{
2022-01-19 13:28:16 +08:00
ModsJson = APIMods . Length > 0
? JsonConvert . SerializeObject ( APIMods )
: string . Empty ;
2022-01-13 11:59:16 +08:00
}
2021-12-06 14:35:08 +08:00
public IEnumerable < HitResultDisplayStatistic > GetStatisticsForDisplay ( )
{
foreach ( var r in Ruleset . CreateInstance ( ) . GetHitResults ( ) )
{
int value = Statistics . GetValueOrDefault ( r . result ) ;
switch ( r . result )
{
case HitResult . SmallTickHit :
case HitResult . LargeTickHit :
2024-01-02 16:01:32 +08:00
case HitResult . SliderTailHit :
2022-11-21 13:20:36 +08:00
case HitResult . LargeBonus :
case HitResult . SmallBonus :
if ( MaximumStatistics . TryGetValue ( r . result , out int count ) & & count > 0 )
2023-10-12 13:55:16 +08:00
yield return new HitResultDisplayStatistic ( r . result , value , count , r . displayName ) ;
2022-11-21 13:20:36 +08:00
break ;
2021-12-06 14:35:08 +08:00
case HitResult . SmallTickMiss :
case HitResult . LargeTickMiss :
break ;
default :
yield return new HitResultDisplayStatistic ( r . result , value , null , r . displayName ) ;
break ;
}
}
}
#endregion
2022-01-13 15:56:09 +08:00
2022-12-16 17:16:26 +08:00
public bool Equals ( ScoreInfo ? other ) = > other ? . ID = = ID ;
2022-01-17 12:51:30 +08:00
2022-01-13 15:56:09 +08:00
public override string ToString ( ) = > this . GetDisplayTitle ( ) ;
2016-11-29 14:41:48 +08:00
}
}