1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 19:27:24 +08:00

Replace EF ScoreInfo with realm version

May contain errors.
This commit is contained in:
Dean Herbert 2021-12-06 15:31:40 +09:00
parent c5e401d678
commit 3da762e145
5 changed files with 310 additions and 312 deletions

View File

@ -26,7 +26,7 @@ namespace osu.Game.Database
public DbSet<FileInfo> FileInfo { get; set; }
public DbSet<EFRulesetInfo> RulesetInfo { get; set; }
public DbSet<EFSkinInfo> SkinInfo { get; set; }
public DbSet<ScoreInfo> ScoreInfo { get; set; }
public DbSet<EFScoreInfo> ScoreInfo { get; set; }
// migrated to realm
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }

View File

@ -1,68 +0,0 @@
// 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.
using System;
using System.Collections.Generic;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
using Realms;
#nullable enable
namespace osu.Game.Models
{
[ExcludeFromDynamicCompile]
[MapTo("Score")]
public class RealmScore : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<RealmScore>, IScoreInfo
{
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
public IList<RealmNamedFileUsage> Files { get; } = null!;
public string Hash { get; set; } = string.Empty;
public bool DeletePending { get; set; }
public bool Equals(RealmScore other) => other.ID == ID;
[Indexed]
public long OnlineID { get; set; } = -1;
public RealmUser User { get; set; } = null!;
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; } = null;
public RealmBeatmap Beatmap { get; set; } = null!;
public RealmRuleset Ruleset { 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<INamedFileUsage> IHasNamedFiles.Files => Files;
}
}

View File

@ -0,0 +1,265 @@
// 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.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
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;
namespace osu.Game.Scoring
{
public class EFScoreInfo : IScoreInfo, IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<EFScoreInfo>, IDeepCloneable<EFScoreInfo>
{
public int ID { get; set; }
public ScoreRank Rank { get; set; }
public long TotalScore { get; set; }
[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; }
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
public double? PP { get; set; }
public int MaxCombo { get; set; }
public int Combo { get; set; } // Todo: Shouldn't exist in here
public int RulesetID { get; set; }
[NotMapped]
public bool Passed { get; set; } = true;
public RulesetInfo Ruleset { get; set; }
private APIMod[] localAPIMods;
private Mod[] mods;
[NotMapped]
public Mod[] Mods
{
get
{
var rulesetInstance = Ruleset?.CreateInstance();
if (rulesetInstance == null)
return mods ?? Array.Empty<Mod>();
Mod[] scoreMods = Array.Empty<Mod>();
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.
[NotMapped]
public APIMod[] APIMods
{
get
{
if (localAPIMods != null)
return localAPIMods;
if (mods == null)
return Array.Empty<APIMod>();
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.
[Column("Mods")]
public string ModsJson
{
get => JsonConvert.SerializeObject(APIMods);
set => APIMods = JsonConvert.DeserializeObject<APIMod[]>(value);
}
[NotMapped]
public APIUser User { get; set; }
[Column("User")]
public string UserString
{
get => User?.Username;
set
{
User ??= new APIUser();
User.Username = value;
}
}
[Column("UserID")]
public int? UserID
{
get => User?.Id ?? 1;
set
{
User ??= new APIUser();
User.Id = value ?? 1;
}
}
public int BeatmapInfoID { get; set; }
[Column("Beatmap")]
public BeatmapInfo BeatmapInfo { get; set; }
public long? OnlineScoreID { get; set; }
public DateTimeOffset Date { get; set; }
[NotMapped]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
[Column("Statistics")]
public string StatisticsJson
{
get => JsonConvert.SerializeObject(Statistics);
set
{
if (value == null)
{
Statistics.Clear();
return;
}
Statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(value);
}
}
[NotMapped]
public List<HitEvent> HitEvents { get; set; }
public List<ScoreFileInfo> Files { get; } = new List<ScoreFileInfo>();
public string Hash { get; set; }
public bool DeletePending { get; set; }
/// <summary>
/// The position of this score, starting at 1.
/// </summary>
[NotMapped]
public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone.
/// <summary>
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[NotMapped]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
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;
}
}
}
public EFScoreInfo DeepClone()
{
var clone = (EFScoreInfo)MemberwiseClone();
clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
return clone;
}
public override string ToString() => this.GetDisplayTitle();
public bool Equals(EFScoreInfo 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);
}
#region Implementation of IHasOnlineID
public long OnlineID => OnlineScoreID ?? -1;
#endregion
#region Implementation of IScoreInfo
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
IUser IScoreInfo.User => User;
bool IScoreInfo.HasReplay => Files.Any();
#endregion
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
}
}

View File

@ -3,266 +3,66 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
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.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Models;
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
{
public class ScoreInfo : IScoreInfo, IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>, IDeepCloneable<ScoreInfo>
[ExcludeFromDynamicCompile]
[MapTo("Score")]
public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<ScoreInfo>, IScoreInfo
{
public int ID { get; set; }
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
public bool IsManaged => ID > 0;
public IList<RealmNamedFileUsage> Files { get; } = null!;
public ScoreRank Rank { get; set; }
public long TotalScore { get; set; }
[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; }
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
public double? PP { get; set; }
public int MaxCombo { get; set; }
public int Combo { get; set; } // Todo: Shouldn't exist in here
public int RulesetID { get; set; }
[NotMapped]
public bool Passed { get; set; } = true;
public RulesetInfo Ruleset { get; set; }
private APIMod[] localAPIMods;
private Mod[] mods;
[NotMapped]
public Mod[] Mods
{
get
{
var rulesetInstance = Ruleset?.CreateInstance();
if (rulesetInstance == null)
return mods ?? Array.Empty<Mod>();
Mod[] scoreMods = Array.Empty<Mod>();
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.
[NotMapped]
public APIMod[] APIMods
{
get
{
if (localAPIMods != null)
return localAPIMods;
if (mods == null)
return Array.Empty<APIMod>();
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.
[Column("Mods")]
public string ModsJson
{
get => JsonConvert.SerializeObject(APIMods);
set => APIMods = JsonConvert.DeserializeObject<APIMod[]>(value);
}
[NotMapped]
public APIUser User { get; set; }
[Column("User")]
public string UserString
{
get => User?.Username;
set
{
User ??= new APIUser();
User.Username = value;
}
}
[Column("UserID")]
public int? UserID
{
get => User?.Id ?? 1;
set
{
User ??= new APIUser();
User.Id = value ?? 1;
}
}
public int BeatmapInfoID { get; set; }
[Column("Beatmap")]
public BeatmapInfo BeatmapInfo { get; set; }
private long? onlineID;
[Column("OnlineScoreID")]
public long? OnlineID
{
get => onlineID;
set => onlineID = value > 0 ? value : null;
}
public DateTimeOffset Date { get; set; }
[NotMapped]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
[Column("Statistics")]
public string StatisticsJson
{
get => JsonConvert.SerializeObject(Statistics);
set
{
if (value == null)
{
Statistics.Clear();
return;
}
Statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(value);
}
}
[NotMapped]
public List<HitEvent> HitEvents { get; set; }
public List<ScoreFileInfo> Files { get; } = new List<ScoreFileInfo>();
public string Hash { get; set; }
public string Hash { get; set; } = string.Empty;
public bool DeletePending { get; set; }
/// <summary>
/// The position of this score, starting at 1.
/// </summary>
[NotMapped]
public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone.
public bool Equals(ScoreInfo other) => other.ID == ID;
/// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[NotMapped]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
[Indexed]
public long OnlineID { get; set; } = -1;
public IEnumerable<HitResultDisplayStatistic> GetStatisticsForDisplay()
public RealmUser User { get; set; } = null!;
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 RealmBeatmap Beatmap { get; set; } = null!;
public RealmRuleset Ruleset { get; set; } = null!;
public ScoreRank Rank
{
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;
}
}
get => (ScoreRank)RankInt;
set => RankInt = (int)value;
}
public ScoreInfo DeepClone()
{
var clone = (ScoreInfo)MemberwiseClone();
[MapTo(nameof(Rank))]
public int RankInt { get; set; }
clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
return clone;
}
public override string ToString() => this.GetDisplayTitle();
public bool Equals(ScoreInfo other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
if (ID != 0 && other.ID != 0)
return ID == other.ID;
return false;
}
#region Implementation of IHasOnlineID
long IHasOnlineID<long>.OnlineID => OnlineID ?? -1;
#endregion
#region Implementation of IScoreInfo
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
IBeatmapInfo IScoreInfo.Beatmap => Beatmap;
IUser IScoreInfo.User => User;
bool IScoreInfo.HasReplay => Files.Any();
#endregion
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
}
}

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Models;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using Realms;
@ -24,7 +25,7 @@ namespace osu.Game.Stores
/// Handles the storage and retrieval of Scores/WorkingScores.
/// </summary>
[ExcludeFromDynamicCompile]
public class ScoreImporter : RealmArchiveModelImporter<RealmScore>
public class ScoreImporter : RealmArchiveModelImporter<ScoreInfo>
{
private readonly RealmRulesetStore rulesets;
private readonly BeatmapManager beatmaps;
@ -40,7 +41,7 @@ namespace osu.Game.Stores
this.beatmaps = beatmaps;
}
protected override RealmScore? CreateModel(ArchiveReader archive)
protected override ScoreInfo? CreateModel(ArchiveReader archive)
{
using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase))))
{
@ -48,7 +49,7 @@ namespace osu.Game.Stores
{
// TODO: make work.
// return new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).ScoreInfo;
return new RealmScore();
return new ScoreInfo();
}
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
{
@ -58,7 +59,7 @@ namespace osu.Game.Stores
}
}
protected override Task Populate(RealmScore model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
}