1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 00:47:24 +08:00
osu-lazer/osu.Game/Scoring/ScoreInfo.cs

276 lines
8.2 KiB
C#
Raw Normal View History

// 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
using System;
using System.Collections.Generic;
using System.Linq;
using AutoMapper;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Localisation;
using osu.Framework.Testing;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps;
2018-11-28 15:39:08 +08:00
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
2018-11-28 15:33:42 +08:00
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
2018-04-13 17:19:50 +08:00
2018-11-28 15:12:57 +08:00
namespace osu.Game.Scoring
2018-04-13 17:19:50 +08:00
{
[ExcludeFromDynamicCompile]
[MapTo("Score")]
public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<ScoreInfo>, IScoreInfo
2018-04-13 17:19:50 +08:00
{
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
2018-04-13 17:19:50 +08:00
public IList<RealmNamedFileUsage> Files { get; } = null!;
2021-04-21 14:16:28 +08:00
public string Hash { get; set; } = string.Empty;
2018-11-28 15:39:08 +08:00
public bool DeletePending { get; set; }
2018-11-30 15:11:09 +08:00
public bool Equals(ScoreInfo other) => other.ID == ID;
[Indexed]
public long OnlineID { get; set; } = -1;
[MapTo("User")]
public RealmUser RealmUser { get; set; } = new RealmUser();
public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser realmUser)
{
Ruleset = ruleset;
BeatmapInfo = beatmap;
RealmUser = realmUser;
}
[UsedImplicitly]
public ScoreInfo() // TODO: consider removing this and migrating all usages to ctor with parameters.
{
}
// 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;
[IgnoreMap]
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 BeatmapInfo { get; set; } = null!;
public RulesetInfo Ruleset { get; set; } = null!;
private Dictionary<HitResult, int>? statistics;
2021-12-06 14:32:02 +08:00
[Ignored]
public Dictionary<HitResult, int> Statistics
{
get
{
if (statistics != null)
return statistics;
if (!string.IsNullOrEmpty(StatisticsJson))
statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(StatisticsJson);
2021-12-06 14:32:02 +08:00
return statistics ??= new Dictionary<HitResult, int>();
2021-12-06 14:32:02 +08:00
}
set => statistics = value;
2021-12-06 14:32:02 +08:00
}
[MapTo("Statistics")]
public string StatisticsJson { get; set; } = null!;
public ScoreRank Rank
2019-12-03 12:33:42 +08:00
{
get => (ScoreRank)RankInt;
set => RankInt = (int)value;
2019-12-03 12:33:42 +08:00
}
[MapTo(nameof(Rank))]
public int RankInt { get; set; }
2021-10-28 17:23:52 +08:00
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IUser IScoreInfo.User => User;
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
#region Properties required to make things work with existing usages
private APIMod[]? localAPIMods;
private Mod[]? mods;
public Guid BeatmapInfoID => BeatmapInfo.ID;
public int UserID => RealmUser.OnlineID;
public int RulesetID => Ruleset.OnlineID;
[Ignored]
public List<HitEvent> HitEvents { get; set; } = new List<HitEvent>();
public ScoreInfo DeepClone()
{
var clone = (ScoreInfo)MemberwiseClone();
clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
return clone;
}
[Ignored]
public bool Passed { get; set; } = true;
public int Combo { get; set; }
/// <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.
[Ignored]
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
/// <summary>
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[Ignored]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
// Used for database serialisation/deserialisation.
[MapTo("Mods")]
public string ModsJson { get; set; } = string.Empty;
[Ignored]
public Mod[] Mods
{
get
{
Mod[] scoreMods = Array.Empty<Mod>();
if (mods != null)
scoreMods = mods;
else if (localAPIMods != null)
scoreMods = APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray();
return scoreMods;
}
set
{
localAPIMods = null;
mods = value;
ModsJson = JsonConvert.SerializeObject(APIMods);
}
}
// Used for API serialisation/deserialisation.
[Ignored]
public APIMod[] APIMods
{
get
{
if (localAPIMods == null)
{
// prioritise reading from realm backing
if (!string.IsNullOrEmpty(ModsJson))
localAPIMods = JsonConvert.DeserializeObject<APIMod[]>(ModsJson);
// then check mods set via Mods property.
if (mods != null)
localAPIMods = mods.Select(m => new APIMod(m)).ToArray();
}
return localAPIMods ?? Array.Empty<APIMod>();
}
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;
ModsJson = JsonConvert.SerializeObject(APIMods);
}
}
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;
}
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
break;
default:
yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
break;
}
}
}
#endregion
2018-04-13 17:19:50 +08:00
}
}