From c8521b49cd6bcfa3aee3c437da357d49a196844d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jan 2024 18:43:15 +0900 Subject: [PATCH 01/23] Change S rank to require no miss --- .../Scoring/CatchScoreProcessor.cs | 2 +- .../Scoring/OsuScoreProcessor.cs | 18 ++++++++++++++++++ .../Scoring/TaikoScoreProcessor.cs | 18 ++++++++++++++++++ .../Visual/Ranking/TestSceneAccuracyCircle.cs | 19 +++++++++++-------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 161a59c5fd..a12fe7f3e1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Scoring return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); } - public override ScoreRank RankFromAccuracy(double accuracy) + public override ScoreRank RankFromScore(double accuracy, Dictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 97980c6d18..0d62a6e718 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -14,6 +16,22 @@ namespace osu.Game.Rulesets.Osu.Scoring { } + public override ScoreRank RankFromScore(double accuracy, Dictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) > 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + protected override HitEvent CreateHitEvent(JudgementResult result) => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 2fd9f070ec..023cd1f902 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { @@ -33,6 +35,22 @@ namespace osu.Game.Rulesets.Taiko.Scoring * strongScaleValue(result); } + public override ScoreRank RankFromScore(double accuracy, Dictionary results) + { + ScoreRank rank = base.RankFromScore(accuracy, results); + + switch (rank) + { + case ScoreRank.S: + case ScoreRank.X: + if (results.GetValueOrDefault(HitResult.Miss) == 0) + rank = ScoreRank.A; + break; + } + + return rank; + } + public override int GetBaseScoreForResult(HitResult result) { switch (result) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 435dd77120..7aa36429a7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -96,6 +97,14 @@ namespace osu.Game.Tests.Visual.Ranking { var scoreProcessor = ruleset.CreateScoreProcessor(); + var statistics = new Dictionary + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + }; + return new ScoreInfo { User = new APIUser @@ -109,15 +118,9 @@ namespace osu.Game.Tests.Visual.Ranking TotalScore = 2845370, Accuracy = accuracy, MaxCombo = 999, - Rank = scoreProcessor.RankFromAccuracy(accuracy), + Rank = scoreProcessor.RankFromScore(accuracy, statistics), Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } + Statistics = statistics, }; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 80e751422e..95f672f6e6 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -370,7 +370,7 @@ namespace osu.Game.Rulesets.Scoring if (rank.Value == ScoreRank.F) return; - rank.Value = RankFromAccuracy(Accuracy.Value); + rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts); foreach (var mod in Mods.Value.OfType()) rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); } @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public virtual ScoreRank RankFromAccuracy(double accuracy) + public virtual ScoreRank RankFromScore(double accuracy, Dictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b30fc7aee1..427b1cb7cf 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -280,7 +280,7 @@ namespace osu.Game.Scoring.Legacy { scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo); - var rank = currentRuleset.CreateScoreProcessor().RankFromAccuracy(scoreInfo.Accuracy); + var rank = currentRuleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); foreach (var mod in scoreInfo.Mods.OfType()) rank = mod.AdjustRank(rank, scoreInfo.Accuracy); From bb1eab58440ab36c0c5197335a20b72a23961219 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 20:25:36 +0900 Subject: [PATCH 02/23] Update test in line with new rank definitions always being applied --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 6e7c8c3631..e56dff74c3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -293,7 +293,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void AccuracyAndRankOfLazerScorePreserved() + public void AccuracyOfLazerScorePreserved() { var ruleset = new OsuRuleset().RulesetInfo; @@ -322,7 +322,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); - Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); + Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); }); } From 4eba3b5d7093f89f186a343c8f316054edd5552c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 20:48:29 +0900 Subject: [PATCH 03/23] Move `BackgroundDataStoreProcessor` to better namespace --- osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs | 1 + osu.Game/{ => Database}/BackgroundDataStoreProcessor.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/{ => Database}/BackgroundDataStoreProcessor.cs (99%) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 8b066f860f..e22cf2398b 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; diff --git a/osu.Game/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs similarity index 99% rename from osu.Game/BackgroundDataStoreProcessor.cs rename to osu.Game/Database/BackgroundDataStoreProcessor.cs index fc7db13d41..e462e0ccf8 100644 --- a/osu.Game/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays; @@ -22,7 +21,7 @@ using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; -namespace osu.Game +namespace osu.Game.Database { /// /// Performs background updating of data stores at startup. From 644e7d6fe6f845ccf0e3d919229d2e130dd76d1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 20:55:17 +0900 Subject: [PATCH 04/23] Add migration --- .../Database/BackgroundDataStoreProcessor.cs | 58 +++++++++++++++++++ osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index e462e0ccf8..324d699e7c 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -73,6 +73,7 @@ namespace osu.Game.Database processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); + upgradeScoreRanks(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -354,6 +355,7 @@ namespace osu.Game.Database realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; + // TODO: ensure that this is also updating rank (as it will set TotalScoreVersion to latest). StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager); s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); @@ -375,6 +377,62 @@ namespace osu.Game.Database completeNotification(notification, processedCount, scoreIds.Count, failedCount); } + private void upgradeScoreRanks() + { + Logger.Log("Querying for scores that need rank upgrades..."); + + HashSet scoreIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .Select(s => s.ID))); + + Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); + + if (scoreIds.Count == 0) + return; + + var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in scoreIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, scoreIds.Count); + + sleepIfRequired(); + + try + { + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + ScoreInfo s = r.Find(id)!; + // TODO: uncomment when ready + // s.Rank = StandardisedScoreMigrationTools.ComputeRank(s, beatmapManager); + }); + + ++processedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to update rank score {id}: {e}"); + realmAccess.Write(r => r.Find(id)!.BackgroundReprocessingFailed = true); + ++failedCount; + } + } + + completeNotification(notification, processedCount, scoreIds.Count, failedCount); + } + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) { if (notification == null) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 389b20b5c8..c74980abb6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -43,9 +43,10 @@ namespace osu.Game.Scoring.Legacy /// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct /// method. Reconvert all scores. /// + /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// /// - public const int LATEST_VERSION = 30000012; + public const int LATEST_VERSION = 30000013; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From 83f9118b22d64e1b52f213d895cb6e959d88b842 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jan 2024 21:31:17 +0900 Subject: [PATCH 05/23] Adjust results screen to handle S->A rank adjustment when misses are present --- .../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++----- .../Expanded/Accuracy/AccuracyCircle.cs | 39 +++++++++++++++++++ .../Ranking/Expanded/Accuracy/RankBadge.cs | 8 ++-- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ab2e867255..866e20d063 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -71,15 +72,16 @@ namespace osu.Game.Tests.Visual.Ranking private int onlineScoreID = 1; - [TestCase(1, ScoreRank.X)] - [TestCase(0.9999, ScoreRank.S)] - [TestCase(0.975, ScoreRank.S)] - [TestCase(0.925, ScoreRank.A)] - [TestCase(0.85, ScoreRank.B)] - [TestCase(0.75, ScoreRank.C)] - [TestCase(0.5, ScoreRank.D)] - [TestCase(0.2, ScoreRank.D)] - public void TestResultsWithPlayer(double accuracy, ScoreRank rank) + [TestCase(1, ScoreRank.X, 0)] + [TestCase(0.9999, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.S, 0)] + [TestCase(0.975, ScoreRank.A, 1)] + [TestCase(0.925, ScoreRank.A, 5)] + [TestCase(0.85, ScoreRank.B, 9)] + [TestCase(0.75, ScoreRank.C, 11)] + [TestCase(0.5, ScoreRank.D, 21)] + [TestCase(0.2, ScoreRank.D, 51)] + public void TestResultsWithPlayer(double accuracy, ScoreRank rank, int missCount) { TestResultsScreen screen = null; @@ -91,6 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents(); score.Accuracy = accuracy; score.Rank = rank; + score.Statistics[HitResult.Miss] = missCount; return screen = createResultsScreen(score); }); diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 8cbca74466..d2632c70c9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { @@ -111,6 +112,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly double accuracyD; private readonly bool withFlair; + private readonly bool isFailedSDueToMisses; + private RankText failedSRankText; + public AccuracyCircle(ScoreInfo score, bool withFlair = false) { this.score = score; @@ -119,10 +123,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); + + // Some rulesets require no misses to get an S rank. + // if (score.Accuracy >= accuracyS && score.Rank == ScoreRank.A) + // accuracyS = score.Accuracy + 0.0001; + accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); + + isFailedSDueToMisses = score.Accuracy >= accuracyS && score.Rank == ScoreRank.A; } [BackgroundDependencyLoader] @@ -249,6 +260,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { + if (isFailedSDueToMisses) + AddInternal(failedSRankText = new RankText(ScoreRank.S)); + AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), @@ -387,6 +401,31 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); } } + + if (isFailedSDueToMisses) + { + const double adjust_duration = 200; + + using (BeginDelayedSequence(TEXT_APPEAR_DELAY - adjust_duration)) + { + failedSRankText.FadeIn(adjust_duration); + + using (BeginDelayedSequence(adjust_duration)) + { + failedSRankText + .FadeColour(Color4.Red, 800, Easing.Out) + .RotateTo(10, 1000, Easing.Out) + .MoveToY(100, 1000, Easing.In) + .FadeOut(800, Easing.Out); + + accuracyCircle + .FillTo(accuracyS - 0.0001, 70, Easing.OutQuint); + + badges.Single(b => b.Rank == ScoreRank.S) + .FadeOut(70, Easing.OutQuint); + } + } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs index 7af327828e..8aea6045eb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// private readonly double displayPosition; - private readonly ScoreRank rank; + public readonly ScoreRank Rank; private Drawable rankContainer; private Drawable overlay; @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { Accuracy = accuracy; displayPosition = position; - this.rank = rank; + Rank = rank; RelativeSizeAxes = Axes.Both; Alpha = 0; @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Size = new Vector2(28, 14), Children = new[] { - new DrawableRank(rank), + new DrawableRank(Rank), overlay = new CircularContainer { RelativeSizeAxes = Axes.Both, @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = OsuColour.ForRank(rank).Opacity(0.2f), + Colour = OsuColour.ForRank(Rank).Opacity(0.2f), Radius = 10, }, Child = new Box From cb8ec48717b49110bb784ae69028612d546a7a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 19:56:30 +0100 Subject: [PATCH 06/23] Make `RankFromScore()`'s dictionary param readonly Just to make sure nobody tries any "funny" business. --- osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs | 2 +- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 2 +- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index a12fe7f3e1..12a4182bf1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Scoring return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); } - public override ScoreRank RankFromScore(double accuracy, Dictionary results) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 0d62a6e718..4d8381cf42 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Scoring { } - public override ScoreRank RankFromScore(double accuracy, Dictionary results) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { ScoreRank rank = base.RankFromScore(accuracy, results); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 023cd1f902..441a53c05a 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring * strongScaleValue(result); } - public override ScoreRank RankFromScore(double accuracy, Dictionary results) + public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { ScoreRank rank = base.RankFromScore(accuracy, results); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 95f672f6e6..a092829317 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public virtual ScoreRank RankFromScore(double accuracy, Dictionary results) + public virtual ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary results) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; From 20ed7e13e31c4da412e1cffab581e4b7054492cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 19:57:12 +0100 Subject: [PATCH 07/23] Fix back-to-front conditional in taiko processor Would be weird to degrade a score due to *no* misses wouldn't it? --- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 441a53c05a..7e40d575bc 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring { case ScoreRank.S: case ScoreRank.X: - if (results.GetValueOrDefault(HitResult.Miss) == 0) + if (results.GetValueOrDefault(HitResult.Miss) > 0) rank = ScoreRank.A; break; } From 45dc9de1e0d22f83ea2851bf8116c180724193da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 19:58:11 +0100 Subject: [PATCH 08/23] Remove remnant of old implementation of showing "A due to misses" --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index d2632c70c9..be9a3a8a78 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -124,10 +124,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); - // Some rulesets require no misses to get an S rank. - // if (score.Accuracy >= accuracyS && score.Rank == ScoreRank.A) - // accuracyS = score.Accuracy + 0.0001; - accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); From 81a02665b67a4f68dda204be6207acd6d9488cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 18:24:39 +0100 Subject: [PATCH 09/23] Adjust existing test to fail --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index e56dff74c3..234067ccda 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -252,7 +252,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void AccuracyAndRankOfStableScorePreserved() + public void AccuracyOfStableScoreRecomputed() { var memoryStream = new MemoryStream(); @@ -261,15 +261,16 @@ namespace osu.Game.Tests.Beatmaps.Formats // and we want to emulate a stable score here using (var sw = new SerializationWriter(memoryStream, true)) { - sw.Write((byte)0); // ruleset id (osu!) + sw.Write((byte)3); // ruleset id (mania). + // mania is used intentionally as it is the only ruleset wherein default accuracy calculation is changed in lazer sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test sw.Write("username"); // irrelevant to this test sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test - sw.Write((ushort)198); // count300 - sw.Write((ushort)1); // count100 + sw.Write((ushort)1); // count300 + sw.Write((ushort)0); // count100 sw.Write((ushort)0); // count50 - sw.Write((ushort)0); // countGeki + sw.Write((ushort)198); // countGeki (perfects / "rainbow 300s" in mania) sw.Write((ushort)0); // countKatu sw.Write((ushort)1); // countMiss sw.Write(12345678); // total score, irrelevant to this test @@ -287,8 +288,8 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { - Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300))); - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); + Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305))); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); }); } From 3f31593a196a5bb7ce93ca4fafb91f45309043be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:01:18 +0100 Subject: [PATCH 10/23] Add another failing tests covering recomputation of ranks --- .../Formats/LegacyScoreDecoderTest.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 234067ccda..fd99b51a2f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -293,6 +293,47 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } + [Test] + public void RankOfStableScoreUsesLazerDefinitions() + { + var memoryStream = new MemoryStream(); + + // local partial implementation of legacy score encoder + // this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION` + // and we want to emulate a stable score here + using (var sw = new SerializationWriter(memoryStream, true)) + { + sw.Write((byte)0); // ruleset id (osu!) + sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable) + sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test + sw.Write("username"); // irrelevant to this test + sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test + sw.Write((ushort)195); // count300 + sw.Write((ushort)1); // count100 + sw.Write((ushort)4); // count50 + sw.Write((ushort)0); // countGeki + sw.Write((ushort)0); // countKatu + sw.Write((ushort)0); // countMiss + sw.Write(12345678); // total score, irrelevant to this test + sw.Write((ushort)1000); // max combo, irrelevant to this test + sw.Write(false); // full combo, irrelevant to this test + sw.Write((int)LegacyMods.Hidden); // mods + sw.Write(string.Empty); // hp graph, irrelevant + sw.Write(DateTime.Now); // date, irrelevant + sw.Write(Array.Empty()); // replay data, irrelevant + sw.Write((long)1234); // legacy online ID, irrelevant + } + + memoryStream.Seek(0, SeekOrigin.Begin); + var decoded = new TestLegacyScoreDecoder().Parse(memoryStream); + + Assert.Multiple(() => + { + // In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer. + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); + }); + } + [Test] public void AccuracyOfLazerScorePreserved() { From aa8eee0796ffcc6e6a244f623f0a98ab69fb2c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 19:00:21 +0100 Subject: [PATCH 11/23] Move maximum statistics population to `LegacyScoreDecoder` --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 68 +++++++++++++++++++ osu.Game/Scoring/ScoreImporter.cs | 67 ------------------ osu.Game/Scoring/ScoreManager.cs | 9 ++- 3 files changed, 76 insertions(+), 68 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 427b1cb7cf..3d671d80c3 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -19,6 +20,7 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy @@ -130,6 +132,8 @@ namespace osu.Game.Scoring.Legacy } } + PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); + if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null) PopulateLegacyAccuracyAndRank(score.ScoreInfo); else @@ -170,6 +174,70 @@ namespace osu.Game.Scoring.Legacy } } + /// + /// Populates the for a given . + /// + /// The score to populate the statistics of. + /// The corresponding . + internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap) + { + Debug.Assert(score.BeatmapInfo != null); + + if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) + return; + + var ruleset = score.Ruleset.Detach(); + var rulesetInstance = ruleset.CreateInstance(); + var scoreProcessor = rulesetInstance.CreateScoreProcessor(); + + Debug.Assert(rulesetInstance != null); + + // Populate the maximum statistics. + HitResult maxBasicResult = rulesetInstance.GetHitResults() + .Select(h => h.result) + .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); + + foreach ((HitResult result, int count) in score.Statistics) + { + switch (result) + { + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; + break; + + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; + break; + + case HitResult.IgnoreHit: + case HitResult.IgnoreMiss: + case HitResult.SmallBonus: + case HitResult.LargeBonus: + break; + + default: + score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; + break; + } + } + + if (!score.IsLegacyScore) + return; + +#pragma warning disable CS0618 + // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. + // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. + var calculator = rulesetInstance.CreateDifficultyCalculator(workingBeatmap); + var attributes = calculator.Calculate(score.Mods); + + int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); + if (attributes.MaxCombo > maxComboFromStatistics) + score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; +#pragma warning restore CS0618 + } + /// /// Populates the accuracy of a given from its contained statistics. /// diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 8e28707107..afa522b253 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -17,7 +17,6 @@ using osu.Game.Scoring.Legacy; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Scoring; using Realms; namespace osu.Game.Scoring @@ -91,8 +90,6 @@ namespace osu.Game.Scoring ArgumentNullException.ThrowIfNull(model.BeatmapInfo); ArgumentNullException.ThrowIfNull(model.Ruleset); - PopulateMaximumStatistics(model); - if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); @@ -110,70 +107,6 @@ namespace osu.Game.Scoring } } - /// - /// Populates the for a given . - /// - /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) - { - Debug.Assert(score.BeatmapInfo != null); - - if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0) - return; - - var beatmap = score.BeatmapInfo!.Detach(); - var ruleset = score.Ruleset.Detach(); - var rulesetInstance = ruleset.CreateInstance(); - var scoreProcessor = rulesetInstance.CreateScoreProcessor(); - - Debug.Assert(rulesetInstance != null); - - // Populate the maximum statistics. - HitResult maxBasicResult = rulesetInstance.GetHitResults() - .Select(h => h.result) - .Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult); - - foreach ((HitResult result, int count) in score.Statistics) - { - switch (result) - { - case HitResult.LargeTickHit: - case HitResult.LargeTickMiss: - score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count; - break; - - case HitResult.SmallTickHit: - case HitResult.SmallTickMiss: - score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count; - break; - - case HitResult.IgnoreHit: - case HitResult.IgnoreMiss: - case HitResult.SmallBonus: - case HitResult.LargeBonus: - break; - - default: - score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count; - break; - } - } - - if (!score.IsLegacyScore) - return; - -#pragma warning disable CS0618 - // In osu! and osu!mania, some judgements affect combo but aren't stored to scores. - // A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes. - var calculator = rulesetInstance.CreateDifficultyCalculator(beatmaps().GetWorkingBeatmap(beatmap)); - var attributes = calculator.Calculate(score.Mods); - - int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum(); - if (attributes.MaxCombo > maxComboFromStatistics) - score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics; -#pragma warning restore CS0618 - } - // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). private readonly Dictionary usernameLookupCache = new Dictionary(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 02d9e0a280..1ee99e9e93 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -26,6 +27,7 @@ namespace osu.Game.Scoring { public class ScoreManager : ModelManager, IModelImporter { + private readonly Func beatmaps; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; @@ -44,6 +46,7 @@ namespace osu.Game.Scoring OsuConfigManager configManager = null) : base(storage, realm) { + this.beatmaps = beatmaps; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api) @@ -171,7 +174,11 @@ namespace osu.Game.Scoring /// Populates the for a given . /// /// The score to populate the statistics of. - public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score); + public void PopulateMaximumStatistics(ScoreInfo score) + { + Debug.Assert(score.BeatmapInfo != null); + LegacyScoreDecoder.PopulateMaximumStatistics(score, beatmaps().GetWorkingBeatmap(score.BeatmapInfo.Detach())); + } #region Implementation of IPresentImports From 2958631c5d69fdd3af915d04f26825077213069e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jan 2024 19:14:53 +0100 Subject: [PATCH 12/23] Use lazer accuracy & rank implementations across the board --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 11 +++++++++++ osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 10 ++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 8c73806cb5..4221ab93f1 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -597,6 +597,17 @@ namespace osu.Game.Database return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; } + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) + { + Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); + var rank = ruleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + + foreach (var mod in scoreInfo.Mods.OfType()) + rank = mod.AdjustRank(rank, scoreInfo.Accuracy); + + return rank; + } + /// /// Used to populate the model using data parsed from its corresponding replay file. /// diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 3d671d80c3..be704ca18b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -40,7 +40,6 @@ namespace osu.Game.Scoring.Legacy }; WorkingBeatmap workingBeatmap; - byte[] compressedScoreInfo = null; using (SerializationReader sr = new SerializationReader(stream)) { @@ -109,6 +108,8 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.LegacyOnlineID = sr.ReadInt32(); + byte[] compressedScoreInfo = null; + if (version >= 30000001) compressedScoreInfo = sr.ReadByteArray(); @@ -133,11 +134,8 @@ namespace osu.Game.Scoring.Legacy } PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); - - if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null) - PopulateLegacyAccuracyAndRank(score.ScoreInfo); - else - populateLazerAccuracyAndRank(score.ScoreInfo); + score.ScoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(score.ScoreInfo); + score.ScoreInfo.Rank = StandardisedScoreMigrationTools.ComputeRank(score.ScoreInfo); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. From db4849442ec7b93312afc39bbc009d4b1da62d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:23:37 +0100 Subject: [PATCH 13/23] Unify legacy total score / accuracy / rank recomputation flows --- .../Database/BackgroundDataStoreProcessor.cs | 6 +- .../StandardisedScoreMigrationTools.cs | 60 +++++++++++-------- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 15 +---- osu.Game/Scoring/ScoreImporter.cs | 5 -- 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 324d699e7c..1e44141819 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -355,8 +355,7 @@ namespace osu.Game.Database realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; - // TODO: ensure that this is also updating rank (as it will set TotalScoreVersion to latest). - StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager); + StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager.GetWorkingBeatmap(s.BeatmapInfo)); s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); @@ -412,8 +411,7 @@ namespace osu.Game.Database realmAccess.Write(r => { ScoreInfo s = r.Find(id)!; - // TODO: uncomment when ready - // s.Rank = StandardisedScoreMigrationTools.ComputeRank(s, beatmapManager); + s.Rank = StandardisedScoreMigrationTools.ComputeRank(s); }); ++processedCount; diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 4221ab93f1..d62d4a905a 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -235,39 +234,49 @@ namespace osu.Game.Database /// Updates a legacy to standardised scoring. /// /// The score to update. - /// A used for lookups. - public static void UpdateFromLegacy(ScoreInfo score, BeatmapManager beatmaps) + /// The applicable for this score. + public static void UpdateFromLegacy(ScoreInfo score, WorkingBeatmap beatmap) { - score.TotalScore = convertFromLegacyTotalScore(score, beatmaps); - score.Accuracy = ComputeAccuracy(score); + var ruleset = score.Ruleset.CreateInstance(); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); } /// /// Updates a legacy to standardised scoring. /// + /// + /// This overload is intended for server-side flows. + /// See: https://github.com/ppy/osu-queue-score-statistics/blob/3681e92ac91c6c61922094bdbc7e92e6217dd0fc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs + /// /// The score to update. + /// The in which the score was set. /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. - public static void UpdateFromLegacy(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + public static void UpdateFromLegacy(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { - score.TotalScore = convertFromLegacyTotalScore(score, difficulty, attributes); - score.Accuracy = ComputeAccuracy(score); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); + score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Rank = computeRank(score, scoreProcessor); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. - /// A used for lookups. + /// The in which the score was set. + /// The applicable for this score. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) { if (!score.IsLegacyScore) return score.TotalScore; - WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo); - Ruleset ruleset = score.Ruleset.CreateInstance(); - if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; @@ -283,27 +292,28 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); - return convertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); + return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); } /// /// Converts from to the new standardised scoring of . /// /// The score to convert the total score of. + /// The in which the score was set. /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) return score.TotalScore; - Debug.Assert(score.LegacyTotalScore != null); - - Ruleset ruleset = score.Ruleset.CreateInstance(); if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; + // ensure legacy total score is saved for later. + score.LegacyTotalScore = score.TotalScore; + double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); int maximumLegacyAccuracyScore = attributes.AccuracyScore; long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier); @@ -584,11 +594,10 @@ namespace osu.Game.Database } } - public static double ComputeAccuracy(ScoreInfo scoreInfo) - { - Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); - ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + public static double ComputeAccuracy(ScoreInfo scoreInfo) => computeAccuracy(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + { int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) @@ -597,10 +606,11 @@ namespace osu.Game.Database return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; } - public static ScoreRank ComputeRank(ScoreInfo scoreInfo) + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + + private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) { - Ruleset ruleset = scoreInfo.Ruleset.CreateInstance(); - var rank = ruleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); foreach (var mod in scoreInfo.Mods.OfType()) rank = mod.AdjustRank(rank, scoreInfo.Accuracy); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index be704ca18b..c12e8b8474 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -134,8 +134,7 @@ namespace osu.Game.Scoring.Legacy } PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); - score.ScoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(score.ScoreInfo); - score.ScoreInfo.Rank = StandardisedScoreMigrationTools.ComputeRank(score.ScoreInfo); + StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); // before returning for database import, we must restore the database-sourced BeatmapInfo. // if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception. @@ -342,18 +341,6 @@ namespace osu.Game.Scoring.Legacy } } - private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo) - { - scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo); - - var rank = currentRuleset.CreateScoreProcessor().RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); - - foreach (var mod in scoreInfo.Mods.OfType()) - rank = mod.AdjustRank(rank, scoreInfo.Accuracy); - - scoreInfo.Rank = rank; - } - private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index afa522b253..768c28cc38 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -100,11 +100,6 @@ namespace osu.Game.Scoring // this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score. if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model)) model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model); - else if (model.IsLegacyScore) - { - model.LegacyTotalScore = model.TotalScore; - StandardisedScoreMigrationTools.UpdateFromLegacy(model, beatmaps()); - } } // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). From 217cbf684b249c220b1c867bca92f0fd4e4a0361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:25:29 +0100 Subject: [PATCH 14/23] Remove superfluous recomputation of accuracy --- .../Database/StandardisedScoreMigrationTools.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index d62d4a905a..5fcf35b690 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -240,9 +240,10 @@ namespace osu.Game.Database var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); + // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); } /// @@ -260,9 +261,10 @@ namespace osu.Game.Database { var scoreProcessor = ruleset.CreateScoreProcessor(); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); + // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); + score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } /// @@ -484,14 +486,9 @@ namespace osu.Game.Database break; case 3: - // in the mania case accuracy actually changes between score V1 and score V2 / standardised - // (PERFECT weighting changes from 300 to 305), - // so for better accuracy recompute accuracy locally based on hit statistics and use that instead, - double scoreV2Accuracy = ComputeAccuracy(score); - convertedTotalScore = (long)Math.Round(( 850000 * comboProportion - + 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy) + + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + bonusProportion) * modMultiplier); break; @@ -594,8 +591,6 @@ namespace osu.Game.Database } } - public static double ComputeAccuracy(ScoreInfo scoreInfo) => computeAccuracy(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); - private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) { int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) From cdd6e71d0129bce737995ae888d015589f7e43f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:26:30 +0100 Subject: [PATCH 15/23] Remove legacy computation of accuracy & ranks --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c12e8b8474..a7374d4d4b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -235,112 +235,6 @@ namespace osu.Game.Scoring.Legacy #pragma warning restore CS0618 } - /// - /// Populates the accuracy of a given from its contained statistics. - /// - /// - /// Legacy use only. - /// - /// The to populate. - public static void PopulateLegacyAccuracyAndRank(ScoreInfo score) - { - int countMiss = score.GetCountMiss() ?? 0; - int count50 = score.GetCount50() ?? 0; - int count100 = score.GetCount100() ?? 0; - int count300 = score.GetCount300() ?? 0; - int countGeki = score.GetCountGeki() ?? 0; - int countKatu = score.GetCountKatu() ?? 0; - - switch (score.Ruleset.OnlineID) - { - case 0: - { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1; - - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; - - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 1: - { - int totalHits = count50 + count100 + count300 + countMiss; - score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1; - - float ratio300 = (float)count300 / totalHits; - float ratio50 = (float)count50 / totalHits; - - if (ratio300 == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9) - score.Rank = ScoreRank.A; - else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8) - score.Rank = ScoreRank.B; - else if (ratio300 > 0.6) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 2: - { - int totalHits = count50 + count100 + count300 + countMiss + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300) / totalHits : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.98) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.94) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.85) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - - case 3: - { - int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu; - score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1; - - if (score.Accuracy == 1) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X; - else if (score.Accuracy > 0.95) - score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S; - else if (score.Accuracy > 0.9) - score.Rank = ScoreRank.A; - else if (score.Accuracy > 0.8) - score.Rank = ScoreRank.B; - else if (score.Accuracy > 0.7) - score.Rank = ScoreRank.C; - else - score.Rank = ScoreRank.D; - break; - } - } - } - private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; From d53fb5a1652a3ae50cb89a2e8609da752a319df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 20:52:45 +0100 Subject: [PATCH 16/23] Update assertions to match expected behaviour --- .../Formats/LegacyScoreDecoderTest.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index fd99b51a2f..7e3967dc95 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -10,7 +10,6 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; @@ -23,6 +22,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; @@ -59,14 +59,14 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); - Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore); Assert.AreEqual(3, score.ScoreInfo.MaxCombo); Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic)); Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL")); Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL")); - Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001)); Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); Assert.That(score.Replay.Frames, Is.Not.Empty); @@ -289,7 +289,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305))); - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); }); } @@ -330,12 +330,12 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { // In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer. - Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.S)); + Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH)); }); } [Test] - public void AccuracyOfLazerScorePreserved() + public void AccuracyRankAndTotalScoreOfLazerScorePreserved() { var ruleset = new OsuRuleset().RulesetInfo; @@ -363,6 +363,8 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(284_537)); + Assert.That(decodedAfterEncode.ScoreInfo.LegacyTotalScore, Is.Null); Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30))); Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A)); }); @@ -457,6 +459,12 @@ namespace osu.Game.Tests.Beatmaps.Formats Ruleset = new OsuRuleset().RulesetInfo, Difficulty = new BeatmapDifficulty(), BeatmapVersion = beatmapVersion, + }, + // needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die + // when trying to recompute total score. + HitObjects = + { + new HitCircle() } }); } From 24be6d92ceebf8950bb7bddd44710eeaac92f10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 21:13:06 +0100 Subject: [PATCH 17/23] Expand xmldoc --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 5fcf35b690..bf92dbc8f5 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -231,7 +231,10 @@ namespace osu.Game.Database } /// - /// Updates a legacy to standardised scoring. + /// Updates a to standardised scoring. + /// This will recompite the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . /// /// The score to update. /// The applicable for this score. @@ -247,7 +250,10 @@ namespace osu.Game.Database } /// - /// Updates a legacy to standardised scoring. + /// Updates a to standardised scoring. + /// This will recompute the score's (always), (always), + /// and (if the score comes from stable). + /// The total score from stable - if any applicable - will be stored to . /// /// /// This overload is intended for server-side flows. From fdd499a4859bcd682dc09156b3dcef1a8a32bcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 21:34:28 +0100 Subject: [PATCH 18/23] Fix rank background processing not working (and not bumping score version) --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 1e44141819..27d19f86c8 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -383,6 +383,7 @@ namespace osu.Game.Database HashSet scoreIds = realmAccess.Run(r => new HashSet( r.All() .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) + .AsEnumerable() // need to materialise here as realm cannot support `.Select()`. .Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades."); @@ -412,6 +413,7 @@ namespace osu.Game.Database { ScoreInfo s = r.Find(id)!; s.Rank = StandardisedScoreMigrationTools.ComputeRank(s); + s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION; }); ++processedCount; From 4d253ebf3c98cc8b923695c7a94b4360143d7d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jan 2024 22:01:03 +0100 Subject: [PATCH 19/23] Remove redundant assertion It does look pretty dumb to be checking if something is `null` *after calling an instance method on it*... --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a7374d4d4b..63bd822205 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -187,8 +187,6 @@ namespace osu.Game.Scoring.Legacy var rulesetInstance = ruleset.CreateInstance(); var scoreProcessor = rulesetInstance.CreateScoreProcessor(); - Debug.Assert(rulesetInstance != null); - // Populate the maximum statistics. HitResult maxBasicResult = rulesetInstance.GetHitResults() .Select(h => h.result) From 0cf90677e616793ca3ea7e7da1c1a74b7b80e7b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jan 2024 19:16:55 +0900 Subject: [PATCH 20/23] Apply more correct visual offset adjustment Co-authored-by: Walavouchey <36758269+Walavouchey@users.noreply.github.com> --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index be9a3a8a78..e7e54d0fae 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -415,7 +415,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy .FadeOut(800, Easing.Out); accuracyCircle - .FillTo(accuracyS - 0.0001, 70, Easing.OutQuint); + .FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); badges.Single(b => b.Rank == ScoreRank.S) .FadeOut(70, Easing.OutQuint); From cb87d6ce500bd8070b1a0ad69537a6f643e1aa67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 11:05:29 +0100 Subject: [PATCH 21/23] Move transferal of `LegacyTotalScore` back to original spot This subtle detail was messing with server-side score import flows. Server-side, legacy total score will *already* be in `LegacyTotalScore` from the start, and `TotalScore` will be zero until recomputed via `StandardisedScoreMigrationTools.UpdateFromLegacy()` - so in that context, attempting to move it across is incorrect. --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 6 +++--- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index bf92dbc8f5..403e73ab77 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -316,12 +317,11 @@ namespace osu.Game.Database if (!score.IsLegacyScore) return score.TotalScore; + Debug.Assert(score.LegacyTotalScore != null); + if (ruleset is not ILegacyRuleset legacyRuleset) return score.TotalScore; - // ensure legacy total score is saved for later. - score.LegacyTotalScore = score.TotalScore; - double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); int maximumLegacyAccuracyScore = attributes.AccuracyScore; long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 63bd822205..e51a95798b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -134,6 +134,10 @@ namespace osu.Game.Scoring.Legacy } PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap); + + if (score.ScoreInfo.IsLegacyScore) + score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore; + StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); // before returning for database import, we must restore the database-sourced BeatmapInfo. From 0b5be3c40cd0ccb2c1ab190c719d44d6d4521268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 12:53:16 +0100 Subject: [PATCH 22/23] Remove outdated test Non-legacy scores *are* subject to upgrades again - albeit it is *rank* upgrades that they are subject to. --- .../BackgroundDataStoreProcessorTests.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index e22cf2398b..e960995c45 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -211,31 +211,6 @@ namespace osu.Game.Tests.Database AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001)); } - [Test] - public void TestNonLegacyScoreNotSubjectToUpgrades() - { - ScoreInfo scoreInfo = null!; - TestBackgroundDataStoreProcessor processor = null!; - - AddStep("Add score which requires upgrade (and has beatmap)", () => - { - Realm.Write(r => - { - r.Add(scoreInfo = new ScoreInfo(ruleset: r.All().First(), beatmap: r.All().First()) - { - TotalScoreVersion = 30000005, - LegacyTotalScore = 123456, - }); - }); - }); - - AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); - AddUntilStep("Wait for completion", () => processor.Completed); - - AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); - AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005)); - } - public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor { protected override int TimeToSleepDuringGameplay => 10; From 6c169e3156af665059df1a526495eee5cec6aa6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jan 2024 12:59:35 +0100 Subject: [PATCH 23/23] Do not reprocess ranks for custom rulesets Chances are that because we've broken rank API, it would utterly fail for all custom rulesets anyhow. --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 27d19f86c8..be0c83bdb3 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -383,7 +383,10 @@ namespace osu.Game.Database HashSet scoreIds = realmAccess.Run(r => new HashSet( r.All() .Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION) - .AsEnumerable() // need to materialise here as realm cannot support `.Select()`. + .AsEnumerable() + // must be done after materialisation, as realm doesn't support + // filtering on nested property predicates or projection via `.Select()` + .Where(s => s.Ruleset.IsLegacyRuleset()) .Select(s => s.ID))); Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades.");