From 807443b6481223fc14574b6c0829e4f197dee6cb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 30 Dec 2023 10:21:59 +0900 Subject: [PATCH 1/5] Add HitResult.SliderTailHit --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 8 ++++++++ osu.Game/Rulesets/Judgements/Judgement.cs | 2 ++ osu.Game/Rulesets/Scoring/HitResult.cs | 15 +++++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +++ 4 files changed, 28 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index d883b91abc..567bf6968f 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -84,6 +84,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)] + [TestCase(ScoringMode.Standardised, HitResult.SliderTailHit, HitResult.SliderTailHit, 326_963)] [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] @@ -96,6 +97,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)] + [TestCase(ScoringMode.Classic, HitResult.SliderTailHit, HitResult.SliderTailHit, 32_696)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) @@ -167,6 +169,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.Perfect, HitResult.Miss)] [TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)] [TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)] + [TestCase(HitResult.SliderTailHit, HitResult.LargeTickMiss)] [TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)] [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)] public void TestMinResults(HitResult hitResult, HitResult expectedMinResult) @@ -187,6 +190,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, false)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, false)] [TestCase(HitResult.LargeBonus, false)] public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue) @@ -207,6 +211,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, false)] [TestCase(HitResult.LargeBonus, false)] public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue) @@ -227,6 +232,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, false)] [TestCase(HitResult.LargeTickMiss, false)] [TestCase(HitResult.LargeTickHit, false)] + [TestCase(HitResult.SliderTailHit, false)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsBonus(HitResult hitResult, bool expectedReturnValue) @@ -247,6 +253,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, false)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsHit(HitResult hitResult, bool expectedReturnValue) @@ -267,6 +274,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.SmallTickHit, true)] [TestCase(HitResult.LargeTickMiss, true)] [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SliderTailHit, true)] [TestCase(HitResult.SmallBonus, true)] [TestCase(HitResult.LargeBonus, true)] public void TestIsScorable(HitResult hitResult, bool expectedReturnValue) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 27402d522c..93386de483 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Judgements return HitResult.SmallTickMiss; case HitResult.LargeTickHit: + case HitResult.SliderTailHit: return HitResult.LargeTickMiss; default: @@ -104,6 +105,7 @@ namespace osu.Game.Rulesets.Judgements case HitResult.SmallTickMiss: return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: return DEFAULT_MAX_HEALTH_INCREASE; diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 2d55f1a649..f307344347 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -139,6 +139,13 @@ namespace osu.Game.Rulesets.Scoring [Order(15)] ComboBreak, + /// + /// A special judgement similar to that's used to increase the valuation of the final tick of a slider. + /// + [EnumMember(Value = "slider_tail_hit")] + [Order(16)] + SliderTailHit, + /// /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// @@ -188,6 +195,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickMiss: case HitResult.LegacyComboIncrease: case HitResult.ComboBreak: + case HitResult.SliderTailHit: return true; default: @@ -246,6 +254,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickMiss: case HitResult.SmallTickHit: case HitResult.SmallTickMiss: + case HitResult.SliderTailHit: return true; default: @@ -329,6 +338,9 @@ namespace osu.Game.Rulesets.Scoring case HitResult.ComboBreak: return true; + case HitResult.SliderTailHit: + return true; + default: // Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score. return result >= HitResult.Miss && result < HitResult.IgnoreMiss; @@ -383,6 +395,9 @@ namespace osu.Game.Rulesets.Scoring if (minResult == HitResult.IgnoreMiss) return; + if (maxResult == HitResult.SliderTailHit && minResult != HitResult.LargeTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); + if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 5123668e54..837bb4080e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -322,6 +322,9 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickHit: return 30; + case HitResult.SliderTailHit: + return 150; + case HitResult.Meh: return 50; From 17a531209cf4ca6d2e2386f736779ad084a9fa44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 30 Dec 2023 10:24:59 +0900 Subject: [PATCH 2/5] Use SliderTailHit result for slider tails (non-classic-mod) --- .../TestSceneSliderEarlyHitJudgement.cs | 2 +- .../TestSceneSliderInput.cs | 4 ++-- .../TestSceneSliderLateHitJudgement.cs | 14 +++++++------- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 2 +- .../Scoring/OsuHealthProcessor.cs | 1 + .../Scoring/OsuLegacyHealthProcessor.cs | 1 + 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs index 4ea21e51f6..19883060a0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertTickJudgement(HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 716d2e0756..12be74c4cc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -467,13 +467,13 @@ namespace osu.Game.Rulesets.Osu.Tests private void assertHeadMissTailTracked() { - AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); + AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit)); AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False); } private void assertMidSliderJudgements() { - AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); + AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit)); } private void assertMidSliderJudgementFail() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs index 6ba9c723da..1ba4a60b75 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); assertHeadJudgement(HitResult.Ok); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertTickJudgement(1, HitResult.LargeTickHit); assertTickJudgement(2, HitResult.LargeTickHit); assertTickJudgement(3, HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertAllTickJudgements(HitResult.LargeTickHit); assertRepeatJudgement(HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertRepeatJudgement(HitResult.LargeTickHit); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertAllTickJudgements(HitResult.LargeTickMiss); // This particular test actually starts tracking the slider just before the end, so the tail should be hit because of its leniency. - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -276,7 +276,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertTickJudgement(0, HitResult.LargeTickMiss); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } @@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Tests assertHeadJudgement(HitResult.Meh); assertTickJudgement(0, HitResult.LargeTickMiss); assertTickJudgement(1, HitResult.LargeTickMiss); - assertTailJudgement(HitResult.LargeTickHit); + assertTailJudgement(HitResult.SliderTailHit); assertSliderJudgement(HitResult.IgnoreHit); } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 357476ed30..ceee513412 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class TailJudgement : SliderEndJudgement { - public override HitResult MaxResult => HitResult.LargeTickHit; + public override HitResult MaxResult => HitResult.SliderTailHit; public override HitResult MinResult => HitResult.IgnoreMiss; } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 2eb257b3e6..fe6da9af35 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Scoring // When classic slider mechanics are enabled, this result comes from the tail. return 0.02; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: switch (result.HitObject) { diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs index e383e82b86..57d2f64e2c 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuLegacyHealthProcessor.cs @@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Osu.Scoring increase = 0.02; break; + case HitResult.SliderTailHit: case HitResult.LargeTickHit: // This result comes from either a slider tick or repeat. increase = hitObject is SliderTick ? 0.015 : 0.02; From 5ae5d7f92dc7b002e5b685922707c9cef61edbf9 Mon Sep 17 00:00:00 2001 From: iminlikewithyou Date: Sat, 30 Dec 2023 23:59:47 -0600 Subject: [PATCH 3/5] change floor to round --- osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index b5315feccb..45213b7bdb 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Edit.Timing row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1; row.WaveformOffsetTo(-offset, animated); row.WaveformScale = new Vector2(scale, 1); - row.BeatIndex = (int)Math.Floor(index); + row.BeatIndex = (int)Math.Round(index); index++; } From 94531807e476c774c42df3ef0ef745608eb80534 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jan 2024 17:01:32 +0900 Subject: [PATCH 4/5] Make slider ends show on results screen again --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ osu.Game/Scoring/ScoreInfo.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 5959576b9d..0496d1f680 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -277,6 +277,7 @@ namespace osu.Game.Rulesets.Osu HitResult.LargeTickHit, HitResult.SmallTickHit, + HitResult.SliderTailHit, HitResult.SmallBonus, HitResult.LargeBonus, }; @@ -289,6 +290,7 @@ namespace osu.Game.Rulesets.Osu case HitResult.LargeTickHit: return "slider tick"; + case HitResult.SliderTailHit: case HitResult.SmallTickHit: return "slider end"; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 44795c6fa7..32e4bbbf29 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -350,6 +350,7 @@ namespace osu.Game.Scoring { case HitResult.SmallTickHit: case HitResult.LargeTickHit: + case HitResult.SliderTailHit: case HitResult.LargeBonus: case HitResult.SmallBonus: if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0) From b0cfea491675a012694576168396ba1c433133e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jan 2024 10:11:49 +0100 Subject: [PATCH 5/5] Fix standardised score conversion failing for some taiko scores due to overestimating accuracy portion Standardised score conversion would return a negative total score for https://osu.ppy.sh/scores/taiko/182230346. The underlying reason for this is that the estimation of the accuracy portion for a score can be above the actual accuracy portion in the taiko ruleset. When calculating the maximum accuracy portion achievable, `TaikoLegacyScoreSimulator` will include the extra 300 points from a double hit on a strong hit, per strong hit. However, this double hit is not factored into accuracy. Both of the aforementioned facts mean that in taiko maximumLegacyAccuracyScore * score.Accuracy - which normally in other rulesets can be used pretty reliably as the exact number of points gained from the accuracy portion - is an estimate in the case of taiko, and an _upper_ estimate at that, because it implicitly assumes that the user has also hit `score.Accuracy` percent of all double hits possible in the beatmap. If this assumption is not upheld, then the user will have earned _less_ points than that from the accuracy portion, which means that the combo proportion estimate will go below zero. It is possible that this has happened on other scores before, but did not result in the total score going negative as the accuracy portion gained would have counteracted the effect of that due to being larger in magnitude than the score loss incurred from the negative combo portion. In the case of the score in question this was not the case due to very low accuracy _and_ very low max combo. --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 66c816e796..9cfb9ea957 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -316,7 +316,7 @@ namespace osu.Game.Database // when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot). // In such cases, just assume 0. double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0 - ? ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore) + ? Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore) : 0; // We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.