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;
|
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>
|
|
|
|
|
/// When setting this, make sure to also set <see cref="BeatmapHash"/> to allow relational consistency when a beatmap is potentially changed.
|
|
|
|
|
/// </remarks>
|
2023-07-04 13:50:34 +08:00
|
|
|
|
public BeatmapInfo? BeatmapInfo { get; set; }
|
2022-01-17 12:51:30 +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
|
|
|
|
|
2022-01-17 12:51:30 +08:00
|
|
|
|
public long TotalScore { get; set; }
|
|
|
|
|
|
|
|
|
|
public int MaxCombo { get; set; }
|
|
|
|
|
|
|
|
|
|
public double Accuracy { get; set; }
|
|
|
|
|
|
2022-07-15 15:11:11 +08:00
|
|
|
|
public bool HasReplay => !string.IsNullOrEmpty(Hash);
|
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
|
|
|
|
|
2021-12-06 14:31:40 +08:00
|
|
|
|
[Indexed]
|
|
|
|
|
public long OnlineID { get; set; } = -1;
|
2020-09-25 19:22:59 +08:00
|
|
|
|
|
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-11-25 15:35:42 +08:00
|
|
|
|
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
|
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);
|
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:
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 13:20:36 +08:00
|
|
|
|
case HitResult.LargeBonus:
|
|
|
|
|
case HitResult.SmallBonus:
|
|
|
|
|
if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0)
|
|
|
|
|
yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
}
|