// 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.Linq; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Models; 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.Users; using osu.Game.Utils; using Realms; #nullable enable namespace osu.Game.Scoring { [ExcludeFromDynamicCompile] [MapTo("Score")] public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IScoreInfo { [PrimaryKey] public Guid ID { get; set; } = Guid.NewGuid(); public IList Files { get; } = null!; public string Hash { get; set; } = string.Empty; public bool DeletePending { get; set; } public bool Equals(ScoreInfo other) => other.ID == ID; [Indexed] public long OnlineID { get; set; } = -1; [MapTo("User")] public RealmUser RealmUser { get; set; } = null!; // 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; public APIUser User { get => user ??= new APIUser { Username = RealmUser.Username, Id = RealmUser.OnlineID, }; set { user = value; RealmUser = new RealmUser { OnlineID = user.OnlineID, Username = user.Username }; } } public long TotalScore { get; set; } public int MaxCombo { get; set; } public double Accuracy { get; set; } public bool HasReplay { get; set; } public DateTimeOffset Date { get; set; } public double? PP { get; set; } public BeatmapInfo Beatmap { get; set; } = null!; public RulesetInfo Ruleset { get; set; } = null!; [Ignored] public Dictionary Statistics { // TODO: this is dangerous. a get operation may then modify the dictionary, which would be a fresh copy that is not persisted with the model. // this is already the case in multiple locations. get { if (string.IsNullOrEmpty(StatisticsJson)) return new Dictionary(); return JsonConvert.DeserializeObject>(StatisticsJson) ?? new Dictionary(); } // .. todo // ReSharper disable once ValueParameterNotUsed set => JsonConvert.SerializeObject(StatisticsJson); } [MapTo("Statistics")] public string StatisticsJson { get; set; } = null!; public ScoreRank Rank { get => (ScoreRank)RankInt; set => RankInt = (int)value; } [MapTo(nameof(Rank))] public int RankInt { get; set; } IRulesetInfo IScoreInfo.Ruleset => Ruleset; IBeatmapInfo IScoreInfo.Beatmap => Beatmap; IUser IScoreInfo.User => User; IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages private APIMod[]? localAPIMods; private Mod[]? mods; public Guid BeatmapInfoID => Beatmap.ID; public BeatmapInfo BeatmapInfo { get => Beatmap; set => Beatmap = value; } public int UserID => RealmUser.OnlineID; public int RulesetID => Ruleset.OnlineID; [Ignored] public List HitEvents { get; set; } = new List(); public ScoreInfo DeepClone() { var clone = (ScoreInfo)MemberwiseClone(); clone.Statistics = new Dictionary(clone.Statistics); return clone; } [Ignored] public bool Passed { get; set; } = true; [Ignored] public int Combo { get; set; } /// /// The position of this score, starting at 1. /// [Ignored] public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone. [Ignored] public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy(); /// /// Whether this represents a legacy (osu!stable) score. /// [Ignored] public bool IsLegacyScore => Mods.OfType().Any(); [Ignored] public Mod[] Mods { get { var rulesetInstance = Ruleset.CreateInstance(); 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. [Ignored] 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; } } 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; } } } #endregion } }