// Copyright (c) ppy Pty Ltd . 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.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Utils; namespace osu.Game.Scoring { public class ScoreInfo : IScoreInfo, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable, IDeepCloneable { public int ID { get; set; } public bool IsManaged => ID > 0; public ScoreRank Rank { get; set; } public long TotalScore { get; set; } [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database. public double Accuracy { get; set; } public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy(); public double? PP { get; set; } public int MaxCombo { get; set; } public int Combo { get; set; } // Todo: Shouldn't exist in here public int RulesetID { get; set; } [NotMapped] public bool Passed { get; set; } = true; public RulesetInfo Ruleset { get; set; } private APIMod[] localAPIMods; private Mod[] mods; [NotMapped] public Mod[] Mods { get { var rulesetInstance = Ruleset?.CreateInstance(); if (rulesetInstance == null) return mods ?? Array.Empty(); Mod[] scoreMods = Array.Empty(); if (mods != null) scoreMods = mods; else if (localAPIMods != null) scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); return scoreMods; } set { localAPIMods = null; mods = value; } } // Used for API serialisation/deserialisation. [NotMapped] public APIMod[] APIMods { get { if (localAPIMods != null) return localAPIMods; if (mods == null) return Array.Empty(); return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); } set { localAPIMods = value; // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. mods = null; } } // Used for database serialisation/deserialisation. [Column("Mods")] public string ModsJson { get => JsonConvert.SerializeObject(APIMods); set => APIMods = JsonConvert.DeserializeObject(value); } [NotMapped] public APIUser User { get; set; } [Column("User")] public string UserString { get => User?.Username; set { User ??= new APIUser(); User.Username = value; } } [Column("UserID")] public int? UserID { get => User?.Id ?? 1; set { User ??= new APIUser(); User.Id = value ?? 1; } } public int BeatmapInfoID { get; set; } [Column("Beatmap")] public BeatmapInfo BeatmapInfo { get; set; } public long? OnlineScoreID { get; set; } public DateTimeOffset Date { get; set; } [NotMapped] public Dictionary Statistics { get; set; } = new Dictionary(); [Column("Statistics")] public string StatisticsJson { get => JsonConvert.SerializeObject(Statistics); set { if (value == null) { Statistics.Clear(); return; } Statistics = JsonConvert.DeserializeObject>(value); } } [NotMapped] public List HitEvents { get; set; } public List Files { get; } = new List(); public string Hash { get; set; } public bool DeletePending { get; set; } /// /// The position of this score, starting at 1. /// [NotMapped] public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone. /// /// Whether this represents a legacy (osu!stable) score. /// [NotMapped] public bool IsLegacyScore => Mods.OfType().Any(); public IEnumerable GetStatisticsForDisplay() { foreach (var r in Ruleset.CreateInstance().GetHitResults()) { int value = Statistics.GetValueOrDefault(r.result); switch (r.result) { case HitResult.SmallTickHit: { int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss); if (total > 0) yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); break; } case HitResult.LargeTickHit: { int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss); if (total > 0) yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); break; } case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: break; default: yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName); break; } } } public ScoreInfo DeepClone() { var clone = (ScoreInfo)MemberwiseClone(); clone.Statistics = new Dictionary(clone.Statistics); return clone; } public override string ToString() => this.GetDisplayTitle(); public bool Equals(ScoreInfo other) { if (other == null) return false; if (ID != 0 && other.ID != 0) return ID == other.ID; if (OnlineScoreID.HasValue && other.OnlineScoreID.HasValue) return OnlineScoreID == other.OnlineScoreID; if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) return Hash == other.Hash; return ReferenceEquals(this, other); } #region Implementation of IHasOnlineID public long OnlineID => OnlineScoreID ?? -1; #endregion #region Implementation of IScoreInfo IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; IRulesetInfo IScoreInfo.Ruleset => Ruleset; bool IScoreInfo.HasReplay => Files.Any(); #endregion IEnumerable IHasNamedFiles.Files => Files; } }