// 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 Newtonsoft.Json.Converters; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Users; using osu.Game.Utils; namespace osu.Game.Scoring { public class ScoreInfo : IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable { public int ID { get; set; } [JsonProperty("rank")] [JsonConverter(typeof(StringEnumConverter))] public ScoreRank Rank { get; set; } [JsonProperty("total_score")] public long TotalScore { get; set; } [JsonProperty("accuracy")] [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; } [JsonIgnore] public string DisplayAccuracy => Accuracy.FormatAccuracy(); [JsonProperty(@"pp")] public double? PP { get; set; } [JsonProperty("max_combo")] public int MaxCombo { get; set; } [JsonIgnore] public int Combo { get; set; } // Todo: Shouldn't exist in here [JsonIgnore] public int RulesetID { get; set; } [JsonProperty("passed")] [NotMapped] public bool Passed { get; set; } = true; [JsonIgnore] public virtual RulesetInfo Ruleset { get; set; } private APIMod[] localAPIMods; private Mod[] mods; [JsonIgnore] [NotMapped] public Mod[] Mods { get { if (mods != null) return mods; if (localAPIMods == null) return Array.Empty(); var rulesetInstance = Ruleset.CreateInstance(); return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } set { localAPIMods = null; mods = value; } } // Used for API serialisation/deserialisation. [JsonProperty("mods")] [NotMapped] private 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. [JsonIgnore] [Column("Mods")] public string ModsJson { get => JsonConvert.SerializeObject(apiMods); set => apiMods = JsonConvert.DeserializeObject(value); } [NotMapped] [JsonProperty("user")] public User User { get; set; } [JsonIgnore] [Column("User")] public string UserString { get => User?.Username; set { User ??= new User(); User.Username = value; } } [JsonIgnore] [Column("UserID")] public int? UserID { get => User?.Id ?? 1; set { User ??= new User(); User.Id = value ?? 1; } } [JsonIgnore] public int BeatmapInfoID { get; set; } [JsonIgnore] public virtual BeatmapInfo Beatmap { get; set; } [JsonIgnore] public long? OnlineScoreID { get; set; } [JsonIgnore] public DateTimeOffset Date { get; set; } [JsonProperty("statistics")] public Dictionary Statistics = new Dictionary(); [JsonIgnore] [Column("Statistics")] public string StatisticsJson { get => JsonConvert.SerializeObject(Statistics); set { if (value == null) { Statistics.Clear(); return; } Statistics = JsonConvert.DeserializeObject>(value); } } [NotMapped] [JsonIgnore] public List HitEvents { get; set; } [JsonIgnore] public List Files { get; set; } [JsonIgnore] public string Hash { get; set; } [JsonIgnore] public bool DeletePending { get; set; } /// /// The position of this score, starting at 1. /// [NotMapped] [JsonProperty("position")] public int? Position { get; set; } private bool isLegacyScore; /// /// Whether this represents a legacy (osu!stable) score. /// [JsonIgnore] [NotMapped] public bool IsLegacyScore { get { if (isLegacyScore) return true; // The above check will catch legacy online scores that have an appropriate UserString + UserId. // For non-online scores such as those imported in, a heuristic is used based on the following table: // // Mode | UserString | UserId // --------------- | ---------- | --------- // stable | | 1 // lazer | | // lazer (offline) | Guest | 1 return ID > 0 && UserID == 1 && UserString != "Guest"; } set => isLegacyScore = value; } public IEnumerable GetStatisticsForDisplay() { foreach (var r in Ruleset.CreateInstance().GetHitResults()) { int value = Statistics.GetOrDefault(r.result); switch (r.result) { case HitResult.SmallTickHit: { int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss); if (total > 0) yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); break; } case HitResult.LargeTickHit: { int total = value + Statistics.GetOrDefault(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 override string ToString() => $"{User} playing {Beatmap}"; 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); } } }