// 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 Newtonsoft.Json.Converters; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Online.API.Requests.Responses { [Serializable] public class SoloScoreInfo : IHasOnlineID { [JsonProperty("replay")] public bool HasReplay { get; set; } [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } [JsonProperty("ruleset_id")] public int RulesetID { get; set; } [JsonProperty("build_id")] public int? BuildID { get; set; } [JsonProperty("passed")] public bool Passed { get; set; } [JsonProperty("total_score")] public int TotalScore { get; set; } [JsonProperty("accuracy")] public double Accuracy { get; set; } [JsonProperty("user_id")] public int UserID { get; set; } // TODO: probably want to update this column to match user stats (short)? [JsonProperty("max_combo")] public int MaxCombo { get; set; } [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("rank")] public ScoreRank Rank { get; set; } [JsonProperty("started_at")] public DateTimeOffset? StartedAt { get; set; } [JsonProperty("ended_at")] public DateTimeOffset EndedAt { get; set; } [JsonProperty("mods")] public APIMod[] Mods { get; set; } = Array.Empty(); [JsonIgnore] [JsonProperty("created_at")] public DateTimeOffset CreatedAt { get; set; } [JsonIgnore] [JsonProperty("updated_at")] public DateTimeOffset UpdatedAt { get; set; } [JsonIgnore] [JsonProperty("deleted_at")] public DateTimeOffset? DeletedAt { get; set; } [JsonProperty("statistics")] public Dictionary Statistics { get; set; } = new Dictionary(); [JsonProperty("maximum_statistics")] public Dictionary MaximumStatistics { get; set; } = new Dictionary(); /// /// Used to preserve the total score for legacy scores. /// [JsonProperty("legacy_total_score")] public int? LegacyTotalScore { get; set; } #region osu-web API additions (not stored to database). [JsonProperty("id")] public long? ID { get; set; } [JsonProperty("user")] public APIUser? User { get; set; } [JsonProperty("beatmap")] public APIBeatmap? Beatmap { get; set; } [JsonProperty("beatmapset")] public APIBeatmapSet? BeatmapSet { set { // in the deserialisation case we need to ferry this data across. // the order of properties returned by the API guarantees that the beatmap is populated by this point. if (!(Beatmap is APIBeatmap apiBeatmap)) throw new InvalidOperationException("Beatmap set metadata arrived before beatmap metadata in response"); apiBeatmap.BeatmapSet = value; } } [JsonProperty("pp")] public double? PP { get; set; } public bool ShouldSerializeID() => false; public bool ShouldSerializeUser() => false; public bool ShouldSerializeBeatmap() => false; public bool ShouldSerializeBeatmapSet() => false; public bool ShouldSerializePP() => false; public bool ShouldSerializeOnlineID() => false; public bool ShouldSerializeHasReplay() => false; #endregion public override string ToString() => $"score_id: {ID} user_id: {UserID}"; /// /// Create a from an API score instance. /// /// A ruleset store, used to populate a ruleset instance in the returned score. /// An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap). /// public ScoreInfo ToScoreInfo(RulesetStore rulesets, BeatmapInfo? beatmap = null) { var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally"); var rulesetInstance = ruleset.CreateInstance(); var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray(); var scoreInfo = ToScoreInfo(mods); scoreInfo.Ruleset = ruleset; if (beatmap != null) scoreInfo.BeatmapInfo = beatmap; return scoreInfo; } /// /// Create a from an API score instance. /// /// The mod instances, resolved from a ruleset. /// public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo { OnlineID = OnlineID, User = User ?? new APIUser { Id = UserID }, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, Ruleset = new RulesetInfo { OnlineID = RulesetID }, Passed = Passed, TotalScore = TotalScore, Accuracy = Accuracy, MaxCombo = MaxCombo, Rank = Rank, Statistics = Statistics, MaximumStatistics = MaximumStatistics, Date = EndedAt, Hash = HasReplay ? "online" : string.Empty, // TODO: temporary? Mods = mods, PP = PP, }; /// /// Creates a from a local score for score submission. /// /// The local score. public static SoloScoreInfo ForSubmission(ScoreInfo score) => new SoloScoreInfo { Rank = score.Rank, TotalScore = (int)score.TotalScore, Accuracy = score.Accuracy, PP = score.PP, MaxCombo = score.MaxCombo, RulesetID = score.RulesetID, Passed = score.Passed, Mods = score.APIMods, Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; public long OnlineID => ID ?? -1; } }