Skip to content

Commit

Permalink
Merge pull request #24794 from bdach/score-encoding-cleanup
Browse files Browse the repository at this point in the history
Correctly handle multiple online score ID types
  • Loading branch information
peppy authored Oct 27, 2023
2 parents 4ed3034 + 32fc19e commit 5a9d417
Show file tree
Hide file tree
Showing 23 changed files with 140 additions and 33 deletions.
28 changes: 28 additions & 0 deletions osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,34 @@ public void TestDecodeTaikoReplay()
}
}

[Test]
public void TestDecodeLegacyOnlineID()
{
var decoder = new TestLegacyScoreDecoder();

using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-legacy-online-id.osr"))
{
var score = decoder.Parse(resourceStream);

Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(-1));
Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(255));
}
}

[Test]
public void TestDecodeNewOnlineID()
{
var decoder = new TestLegacyScoreDecoder();

using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr"))
{
var score = decoder.Parse(resourceStream);

Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258));
Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1));
}
}

[TestCase(3, true)]
[TestCase(6, false)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ private void checkState(DownloadState expectedState) =>
OnlineID = hasOnlineId ? online_score_id : 0,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(),
Hash = replayAvailable ? "online" : string.Empty,
HasOnlineReplay = replayAvailable,
User = new APIUser
{
Id = 39828,
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresC
{
var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
score.Hash = $"test{i}";
score.HasOnlineReplay = true;
scores.Add(score);
}

Expand Down
6 changes: 4 additions & 2 deletions osu.Game/Database/IHasRealmFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ namespace osu.Game.Database
/// <summary>
/// A model that contains a list of files it is responsible for.
/// </summary>
public interface IHasRealmFiles
public interface IHasRealmFiles : IHasNamedFiles
{
/// <summary>
/// Available files in this model, with locally filenames.
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
/// </summary>
IList<RealmNamedFileUsage> Files { get; }
new IList<RealmNamedFileUsage> Files { get; }

IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;

/// <summary>
/// A combined hash representing the model, based on the files it contains.
Expand Down
21 changes: 20 additions & 1 deletion osu.Game/Database/RealmAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ public class RealmAccess : IDisposable
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
/// </summary>
private const int schema_version = 35;
private const int schema_version = 36;

/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
Expand Down Expand Up @@ -1075,6 +1076,24 @@ void convertOnlineIDs<T>() where T : RealmObject

break;
}

case 36:
{
foreach (var score in migration.NewRealm.All<ScoreInfo>())
{
if (score.OnlineID > 0)
{
score.LegacyOnlineID = score.OnlineID;
score.OnlineID = -1;
}
else
{
score.LegacyOnlineID = score.OnlineID = -1;
}
}

break;
}
}

Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
Expand Down
20 changes: 18 additions & 2 deletions osu.Game/Extensions/ModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,24 @@ public static string GetDisplayString(this object? model)
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other);
/// <returns>
/// Whether online IDs match.
/// Both <see cref="IHasOnlineID{T}.OnlineID"/> and <see cref="IScoreInfo.LegacyOnlineID"/> are checked, in that order.
/// If either instance is missing an online ID, this will return false.
/// </returns>
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other)
{
if (matchesOnlineID(instance, other))
return true;

if (instance == null || other == null)
return false;

if (instance.LegacyOnlineID < 0 || other.LegacyOnlineID < 0)
return false;

return instance.LegacyOnlineID.Equals(other.LegacyOnlineID);
}

private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
{
Expand Down
21 changes: 16 additions & 5 deletions osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
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;
using osu.Game.Users;

namespace osu.Game.Online.API.Requests.Responses
{
[Serializable]
public class SoloScoreInfo : IHasOnlineID<long>
public class SoloScoreInfo : IScoreInfo
{
[JsonProperty("beatmap_id")]
public int BeatmapID { get; set; }
Expand Down Expand Up @@ -138,6 +138,18 @@ public APIBeatmapSet? BeatmapSet

#endregion

#region IScoreInfo

public long OnlineID => (long?)ID ?? -1;

IUser IScoreInfo.User => User!;
DateTimeOffset IScoreInfo.Date => EndedAt;
long IScoreInfo.LegacyOnlineID => (long?)LegacyScoreId ?? -1;
IBeatmapInfo IScoreInfo.Beatmap => Beatmap!;
IRulesetInfo IScoreInfo.Ruleset => Beatmap!.Ruleset;

#endregion

public override string ToString() => $"score_id: {ID} user_id: {UserID}";

/// <summary>
Expand Down Expand Up @@ -178,6 +190,7 @@ public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
var score = new ScoreInfo
{
OnlineID = OnlineID,
LegacyOnlineID = (long?)LegacyScoreId ?? -1,
User = User ?? new APIUser { Id = UserID },
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Expand All @@ -189,7 +202,7 @@ public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
Statistics = Statistics,
MaximumStatistics = MaximumStatistics,
Date = EndedAt,
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
HasOnlineReplay = HasReplay,
Mods = mods,
PP = PP,
};
Expand Down Expand Up @@ -223,7 +236,5 @@ public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
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 => (long?)ID ?? -1;
}
}
5 changes: 4 additions & 1 deletion osu.Game/Online/Rooms/MultiplayerScore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class MultiplayerScore
[JsonProperty("position")]
public int? Position { get; set; }

[JsonProperty("has_replay")]
public bool HasReplay { get; set; }

/// <summary>
/// Any scores in the room around this score.
/// </summary>
Expand All @@ -84,7 +87,7 @@ public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore ruleset
User = User,
Accuracy = Accuracy,
Date = EndedAt,
Hash = string.Empty, // todo: temporary?
HasOnlineReplay = HasReplay,
Rank = Rank,
Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(),
Position = Position,
Expand Down
4 changes: 3 additions & 1 deletion osu.Game/Online/ScoreDownloadTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ protected override void LoadComplete()
var scoreInfo = new ScoreInfo
{
ID = TrackedItem.ID,
OnlineID = TrackedItem.OnlineID
OnlineID = TrackedItem.OnlineID,
LegacyOnlineID = TrackedItem.LegacyOnlineID
};

Downloader.DownloadBegan += downloadBegan;
Downloader.DownloadFailed += downloadFailed;

realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|| (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID)
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
&& !s.DeletePending), (items, _) =>
{
Expand Down
3 changes: 3 additions & 0 deletions osu.Game/OsuGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ public void PresentScore(IScoreInfo score, ScorePresentType presentType = ScoreP
if (score.OnlineID > 0)
databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);

if (score.LegacyOnlineID > 0)
databasedScoreInfo ??= ScoreManager.Query(s => s.LegacyOnlineID == score.LegacyOnlineID);

if (score is ScoreInfo scoreInfo)
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash);

Expand Down
4 changes: 2 additions & 2 deletions osu.Game/Scoring/IScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace osu.Game.Scoring
{
public interface IScoreInfo : IHasOnlineID<long>, IHasNamedFiles
public interface IScoreInfo : IHasOnlineID<long>
{
IUser User { get; }

Expand All @@ -22,7 +22,7 @@ public interface IScoreInfo : IHasOnlineID<long>, IHasNamedFiles

double Accuracy { get; }

bool HasReplay { get; }
long LegacyOnlineID { get; }

DateTimeOffset Date { get; }

Expand Down
8 changes: 8 additions & 0 deletions osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ namespace osu.Game.Scoring.Legacy
[JsonObject(MemberSerialization.OptIn)]
public class LegacyReplaySoloScoreInfo
{
/// <remarks>
/// The value of this property should correspond to <see cref="ScoreInfo.OnlineID"/>
/// (i.e. come from the `solo_scores` ID scheme).
/// </remarks>
[JsonProperty("online_id")]
public long OnlineID { get; set; } = -1;

[JsonProperty("mods")]
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();

Expand All @@ -30,6 +37,7 @@ public class LegacyReplaySoloScoreInfo

public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
{
OnlineID = score.OnlineID,
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),
Expand Down
5 changes: 3 additions & 2 deletions osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ public Score Parse(Stream stream)
byte[] compressedReplay = sr.ReadByteArray();

if (version >= 20140721)
scoreInfo.OnlineID = sr.ReadInt64();
scoreInfo.LegacyOnlineID = sr.ReadInt64();
else if (version >= 20121008)
scoreInfo.OnlineID = sr.ReadInt32();
scoreInfo.LegacyOnlineID = sr.ReadInt32();

byte[] compressedScoreInfo = null;

Expand All @@ -121,6 +121,7 @@ public Score Parse(Stream stream)

Debug.Assert(readScore != null);

score.ScoreInfo.OnlineID = readScore.OnlineID;
score.ScoreInfo.Statistics = readScore.Statistics;
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void Encode(Stream stream, bool leaveOpen = false)
sw.Write(getHpGraphFormatted());
sw.Write(score.ScoreInfo.Date.DateTime);
sw.WriteByteArray(createReplayData());
sw.Write((long)0);
sw.Write(score.ScoreInfo.LegacyOnlineID);
writeModSpecificData(score.ScoreInfo, sw);
sw.WriteByteArray(createScoreInfoData());
}
Expand Down
20 changes: 18 additions & 2 deletions osu.Game/Scoring/ScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,32 @@ public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftD

public double Accuracy { get; set; }

public bool HasReplay => !string.IsNullOrEmpty(Hash);
[Ignored]
public bool HasOnlineReplay { get; set; }

public DateTimeOffset Date { get; set; }

public double? PP { get; set; }

/// <summary>
/// The online ID of this score.
/// </summary>
/// <remarks>
/// In the osu-web database, this ID (if present) comes from the new <c>solo_scores</c> table.
/// </remarks>
[Indexed]
public long OnlineID { get; set; } = -1;

/// <summary>
/// The legacy online ID of this score.
/// </summary>
/// <remarks>
/// In the osu-web database, this ID (if present) comes from the legacy <c>osu_scores_*_high</c> tables.
/// This ID is also stored to replays set on osu!stable.
/// </remarks>
[Indexed]
public long LegacyOnlineID { get; set; } = -1;

[MapTo("User")]
public RealmUser RealmUser { get; set; } = null!;

Expand Down Expand Up @@ -168,7 +185,6 @@ public ScoreRank Rank
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

Expand Down
6 changes: 5 additions & 1 deletion osu.Game/Scoring/ScoreManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ public void Delete(BeatmapInfo beatmap, bool silent = false)

public Task Import(ImportTask[] imports, ImportParameters parameters = default) => scoreImporter.Import(imports, parameters);

public override bool IsAvailableLocally(ScoreInfo model) => Realm.Run(realm => realm.All<ScoreInfo>().Any(s => s.OnlineID == model.OnlineID));
public override bool IsAvailableLocally(ScoreInfo model)
=> Realm.Run(realm => realm.All<ScoreInfo>()
// this basically inlines `ModelExtension.MatchesOnlineID(IScoreInfo, IScoreInfo)`,
// because that method can't be used here, as realm can't translate it to its query language.
.Any(s => s.OnlineID == model.OnlineID || s.LegacyOnlineID == model.LegacyOnlineID));

public IEnumerable<string> HandledExtensions => scoreImporter.HandledExtensions;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public partial class MultiplayerResultsScreen : PlaylistsResultsScreen
{
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem, false, false)
: base(score, roomId, playlistItem, false)
{
}
}
Expand Down
7 changes: 0 additions & 7 deletions osu.Game/Screens/Play/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,13 +1167,6 @@ protected virtual Task ImportScore(Score score)
// the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.
var importableScore = score.ScoreInfo.DeepClone();

// For the time being, online ID responses are not really useful for anything.
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
//
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
// conflicts across various systems (ie. solo and multiplayer).
importableScore.OnlineID = -1;

var imported = scoreManager.Import(importableScore, replayReader);

imported.PerformRead(s =>
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Screens/Ranking/ReplayDownloadButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private ReplayAvailability replayAvailability
if (State.Value == DownloadState.LocallyAvailable)
return ReplayAvailability.Local;

if (Score.Value?.HasReplay == true)
if (Score.Value?.HasOnlineReplay == true)
return ReplayAvailability.Online;

return ReplayAvailability.NotAvailable;
Expand Down
Loading

0 comments on commit 5a9d417

Please sign in to comment.