From 1eb04c5190220e97b901ab989985709f1553fa22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 Feb 2024 11:24:31 +0100 Subject: [PATCH 01/88] Add failing test coverage for catch --- .../CatchHealthProcessorTest.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs diff --git a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs new file mode 100644 index 0000000000..8c2d1a91ab --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Scoring; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class CatchHealthProcessorTest + { + private static readonly object[][] test_cases = + [ + // hitobject, starting HP, fail expected after miss + [new Fruit(), 0.01, true], + [new Droplet(), 0.01, true], + [new TinyDroplet(), 0, true], + [new Banana(), 0, false], + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected) + { + var healthProcessor = new CatchHealthProcessor(0); + healthProcessor.ApplyBeatmap(new CatchBeatmap + { + HitObjects = { hitObject } + }); + healthProcessor.Health.Value = startingHealth; + + var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MinResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected)); + } + + [TestCaseSource(nameof(test_cases))] + public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _) + { + var healthProcessor = new CatchHealthProcessor(0); + healthProcessor.ApplyBeatmap(new CatchBeatmap + { + HitObjects = { hitObject } + }); + healthProcessor.Health.Value = startingHealth; + + var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MaxResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.False); + } + } +} From 86801aa51d9716228f24fb4728607f95f2ed8f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 Feb 2024 11:57:42 +0100 Subject: [PATCH 02/88] Add failing test coverage for osu! --- .../OsuHealthProcessorTest.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs new file mode 100644 index 0000000000..cf93e0ce7b --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class OsuHealthProcessorTest + { + private static readonly object[][] test_cases = + [ + // hitobject, starting HP, fail expected after miss + [new HitCircle(), 0.01, true], + [new SliderHeadCircle(), 0.01, true], + [new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true], + [new SliderTick(), 0.01, true], + [new SliderRepeat(new Slider()), 0.01, true], + [new SliderTailCircle(new Slider()), 0, true], + [new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true], + [new Slider(), 0, true], + [new Slider { ClassicSliderBehaviour = true }, 0.01, true], + [new SpinnerTick(), 0, false], + [new SpinnerBonusTick(), 0, false], + [new Spinner(), 0.01, true], + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected) + { + var healthProcessor = new OsuHealthProcessor(0); + healthProcessor.ApplyBeatmap(new OsuBeatmap + { + HitObjects = { hitObject } + }); + healthProcessor.Health.Value = startingHealth; + + var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MinResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected)); + } + + [TestCaseSource(nameof(test_cases))] + public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _) + { + var healthProcessor = new OsuHealthProcessor(0); + healthProcessor.ApplyBeatmap(new OsuBeatmap + { + HitObjects = { hitObject } + }); + healthProcessor.Health.Value = startingHealth; + + var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MaxResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.False); + } + } +} From 441a7b3c2ff68fc13fefb439698e4605f9adacc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 Feb 2024 12:07:40 +0100 Subject: [PATCH 03/88] Add precautionary taiko test coverage --- .../TaikoHealthProcessorTest.cs | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs index f4a1e888c9..aba967cdd6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoHealthProcessorTest.cs @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { HitObjects = { - new DrumRoll { Duration = 2000 } + new Swell { Duration = 2000 } } }; @@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests Assert.That(healthProcessor.HasFailed, Is.False); }); } + + [Test] + public void TestMissHitAndHitSwell() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit(), + new Swell { Duration = 2000 } + } + }; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(beatmap); + + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss }); + + foreach (var nested in beatmap.HitObjects[1].NestedHitObjects) + { + var nestedJudgement = nested.CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult }); + } + + var judgement = beatmap.HitObjects[1].CreateJudgement(); + healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult }); + + Assert.Multiple(() => + { + Assert.That(healthProcessor.Health.Value, Is.EqualTo(0)); + Assert.That(healthProcessor.HasFailed, Is.True); + }); + } + + private static readonly object[][] test_cases = + [ + // hitobject, fail expected after miss + [new Hit(), true], + [new Hit.StrongNestedHit(new Hit()), false], + [new DrumRollTick(new DrumRoll()), false], + [new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false], + [new DrumRoll(), false], + [new SwellTick(), false], + [new Swell(), false] + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected) + { + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(new TaikoBeatmap + { + HitObjects = { hitObject } + }); + + var result = new JudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MinResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected)); + } + + [TestCaseSource(nameof(test_cases))] + public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _) + { + var healthProcessor = new TaikoHealthProcessor(); + healthProcessor.ApplyBeatmap(new TaikoBeatmap + { + HitObjects = { hitObject } + }); + + var result = new JudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MaxResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.False); + } } } From d07ea8f5b12886883a60cf9c477fa9d632404ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 Feb 2024 12:17:38 +0100 Subject: [PATCH 04/88] Add failing test coverage for mania --- .../ManiaHealthProcessorTest.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs index 315849f7de..a9771a46f3 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Scoring; @@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests // No matter what, mania doesn't have passive HP drain. Assert.That(processor.DrainRate, Is.Zero); } + + private static readonly object[][] test_cases = + [ + // hitobject, starting HP, fail expected after miss + [new Note(), 0.01, true], + [new HeadNote(), 0.01, true], + [new TailNote(), 0.01, true], + [new HoldNoteBody(), 0, true], // hold note break + [new HoldNote(), 0, true], + ]; + + [TestCaseSource(nameof(test_cases))] + public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected) + { + var healthProcessor = new ManiaHealthProcessor(0); + healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = { hitObject } + }); + healthProcessor.Health.Value = startingHealth; + + var result = new JudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MinResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected)); + } + + [TestCaseSource(nameof(test_cases))] + public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _) + { + var healthProcessor = new ManiaHealthProcessor(0); + healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = { hitObject } + }); + healthProcessor.Health.Value = startingHealth; + + var result = new JudgementResult(hitObject, hitObject.CreateJudgement()); + result.Type = result.Judgement.MaxResult; + healthProcessor.ApplyResult(result); + + Assert.That(healthProcessor.HasFailed, Is.False); + } } } From 16d893d40c10542a2502884df95d54773044ffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 Feb 2024 12:26:37 +0100 Subject: [PATCH 05/88] Fix draining processor failing gameplay on bonus misses and ignore hits --- osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 5 +++++ osu.Game/Rulesets/Scoring/HealthProcessor.cs | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 629a84ea62..92a064385b 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -142,6 +142,11 @@ namespace osu.Game.Rulesets.Scoring } } + protected override bool CanFailOn(JudgementResult result) + { + return !result.Judgement.MaxResult.IsBonus() && result.Type != HitResult.IgnoreHit; + } + protected override void Reset(bool storeResults) { base.Reset(storeResults); diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index b5eb755650..ccf53f075a 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Scoring Health.Value += GetHealthIncreaseFor(result); - if (meetsAnyFailCondition(result)) + if (CanFailOn(result) && meetsAnyFailCondition(result)) TriggerFailure(); } @@ -68,6 +68,13 @@ namespace osu.Game.Rulesets.Scoring /// The health increase. protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease; + /// + /// Whether a failure can occur on a given . + /// If the return value of this method is , neither nor will be checked + /// after this . + /// + protected virtual bool CanFailOn(JudgementResult result) => true; + /// /// The default conditions for failing. /// From 3d8d0f8430487c4c30e8964f11e52e1a6522f098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Feb 2024 08:37:20 +0100 Subject: [PATCH 06/88] Update test expectations for catch --- osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs index 8c2d1a91ab..d0a8ce4bbc 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests // hitobject, starting HP, fail expected after miss [new Fruit(), 0.01, true], [new Droplet(), 0.01, true], - [new TinyDroplet(), 0, true], + [new TinyDroplet(), 0, false], [new Banana(), 0, false], ]; From f53bce8ff785a1c52bcd1bffb365e43287ec8bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Feb 2024 08:38:39 +0100 Subject: [PATCH 07/88] Fix catch health processor allowing fail on tiny droplet Closes https://github.com/ppy/osu/issues/27159. In today's episode of "just stable things"... --- .../Scoring/CatchHealthProcessor.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index c3cc488941..2e1aec0803 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty(); + protected override bool CanFailOn(JudgementResult result) + { + // matches stable. + // see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967 + // the above early-return skips the failure check at the end of the same method: + // https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232 + // making it impossible to fail on a tiny droplet regardless of result. + if (result.Type == HitResult.SmallTickMiss) + return false; + + return base.CanFailOn(result); + } + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) { double increase = 0; From 2c0a5b7ef54169412c805ad4b05ea47cf131c012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Feb 2024 14:21:48 +0100 Subject: [PATCH 08/88] Fix missing tiny droplet not triggering fail with perfect on Stable does this: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameplayElements/HitObjectManagerFruits.cs#L98-L102 I'd rather not say what I think about it doing that, since it's likely to be unpublishable, but to approximate that, just make it so that only the "default fail condition" is beholden to the weird ebbs and flows of what the ruleset wants. This appears to fix the problem case and I'm hoping it doesn't break something else but I'm like 50/50 on it happening anyway at this point. Just gotta add tests add nauseam. --- .../Scoring/CatchHealthProcessor.cs | 4 ++-- .../Scoring/AccumulatingHealthProcessor.cs | 4 +++- .../Scoring/DrainingHealthProcessor.cs | 7 +++++-- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 18 ++++++------------ 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 2e1aec0803..2f55f9a85f 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Scoring protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty(); - protected override bool CanFailOn(JudgementResult result) + protected override bool CheckDefaultFailCondition(JudgementResult result) { // matches stable. // see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967 @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Scoring if (result.Type == HitResult.SmallTickMiss) return false; - return base.CanFailOn(result); + return base.CheckDefaultFailCondition(result); } protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs index 422bf8ea79..bb4c2463a7 100644 --- a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Scoring { /// @@ -9,7 +11,7 @@ namespace osu.Game.Rulesets.Scoring /// public partial class AccumulatingHealthProcessor : HealthProcessor { - protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth; + protected override bool CheckDefaultFailCondition(JudgementResult _) => JudgedHits == MaxHits && Health.Value < requiredHealth; private readonly double requiredHealth; diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 92a064385b..e72a8aaf67 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -142,9 +142,12 @@ namespace osu.Game.Rulesets.Scoring } } - protected override bool CanFailOn(JudgementResult result) + protected override bool CheckDefaultFailCondition(JudgementResult result) { - return !result.Judgement.MaxResult.IsBonus() && result.Type != HitResult.IgnoreHit; + if (result.Judgement.MaxResult.IsBonus() || result.Type == HitResult.IgnoreHit) + return false; + + return base.CheckDefaultFailCondition(result); } protected override void Reset(bool storeResults) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index ccf53f075a..9e4c06b783 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Scoring public event Func? Failed; /// - /// Additional conditions on top of that cause a failing state. + /// Additional conditions on top of that cause a failing state. /// public event Func? FailConditions; @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Scoring Health.Value += GetHealthIncreaseFor(result); - if (CanFailOn(result) && meetsAnyFailCondition(result)) + if (meetsAnyFailCondition(result)) TriggerFailure(); } @@ -69,16 +69,10 @@ namespace osu.Game.Rulesets.Scoring protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease; /// - /// Whether a failure can occur on a given . - /// If the return value of this method is , neither nor will be checked - /// after this . + /// Checks whether the default conditions for failing are met. /// - protected virtual bool CanFailOn(JudgementResult result) => true; - - /// - /// The default conditions for failing. - /// - protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value); + /// if failure should be invoked. + protected virtual bool CheckDefaultFailCondition(JudgementResult result) => Precision.AlmostBigger(Health.MinValue, Health.Value); /// /// Whether the current state of or the provided meets any fail condition. @@ -86,7 +80,7 @@ namespace osu.Game.Rulesets.Scoring /// The judgement result. private bool meetsAnyFailCondition(JudgementResult result) { - if (DefaultFailCondition) + if (CheckDefaultFailCondition(result)) return true; if (FailConditions != null) From cd8ac6a722124e7827f2a8ee0ef39ef2ab41a932 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 17 Feb 2024 22:12:15 +0200 Subject: [PATCH 09/88] Update BeatmapAttributesDisplay.cs --- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index b9e4896b21..9312940001 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Overlays.Mods @@ -42,6 +43,9 @@ namespace osu.Game.Overlays.Mods [Resolved] private Bindable> mods { get; set; } = null!; + [Resolved(CanBeNull = true)] + private IBindable? selectedItem { get; set; } + public BindableBool Collapsed { get; } = new BindableBool(true); private ModSettingChangeTracker? modSettingChangeTracker; @@ -108,6 +112,8 @@ namespace osu.Game.Overlays.Mods updateValues(); }, true); + selectedItem?.BindValueChanged(_ => mods.TriggerChange()); + BeatmapInfo.BindValueChanged(_ => updateValues(), true); Collapsed.BindValueChanged(_ => @@ -164,10 +170,20 @@ namespace osu.Game.Overlays.Mods starRatingDisplay.FinishTransforms(true); }); + Ruleset ruleset = gameRuleset.Value.CreateInstance(); + double rate = 1; foreach (var mod in mods.Value.OfType()) rate = mod.ApplyToRate(0, rate); + if (selectedItem != null && selectedItem.Value != null) + { + var globalMods = selectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + + foreach (var mod in globalMods.OfType()) + rate = mod.ApplyToRate(0, rate); + } + bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); @@ -175,7 +191,6 @@ namespace osu.Game.Overlays.Mods foreach (var mod in mods.Value.OfType()) mod.ApplyToDifficulty(originalDifficulty); - Ruleset ruleset = gameRuleset.Value.CreateInstance(); BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); From ed819fde1589bc0cc6167b4610c5a115e49fc844 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 17 Feb 2024 23:01:31 +0200 Subject: [PATCH 10/88] Fixed bugs and added ranked status update --- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 32 ++++++++++++------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 32 +++++++++++++++++-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 3 +- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 9312940001..da14382175 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -40,11 +40,16 @@ namespace osu.Game.Overlays.Mods public Bindable BeatmapInfo { get; } = new Bindable(); + /// + /// Should attribute display account for the multiplayer room global mods. + /// + public bool AccountForMultiplayerMods = false; + [Resolved] private Bindable> mods { get; set; } = null!; [Resolved(CanBeNull = true)] - private IBindable? selectedItem { get; set; } + private IBindable? multiplayerRoomItem { get; set; } public BindableBool Collapsed { get; } = new BindableBool(true); @@ -112,7 +117,7 @@ namespace osu.Game.Overlays.Mods updateValues(); }, true); - selectedItem?.BindValueChanged(_ => mods.TriggerChange()); + multiplayerRoomItem?.BindValueChanged(_ => mods.TriggerChange()); BeatmapInfo.BindValueChanged(_ => updateValues(), true); @@ -176,23 +181,26 @@ namespace osu.Game.Overlays.Mods foreach (var mod in mods.Value.OfType()) rate = mod.ApplyToRate(0, rate); - if (selectedItem != null && selectedItem.Value != null) - { - var globalMods = selectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - - foreach (var mod in globalMods.OfType()) - rate = mod.ApplyToRate(0, rate); - } - - bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; - BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); foreach (var mod in mods.Value.OfType()) mod.ApplyToDifficulty(originalDifficulty); + if (AccountForMultiplayerMods && multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + { + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + + foreach (var mod in multiplayerRoomMods.OfType()) + rate = mod.ApplyToRate(0, rate); + + foreach (var mod in multiplayerRoomMods.OfType()) + mod.ApplyToDifficulty(originalDifficulty); + } + BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; + TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ddf96c1cb3..0804624ed8 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -25,7 +25,9 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Online.Rooms; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -42,6 +44,12 @@ namespace osu.Game.Overlays.Mods [Cached] public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); + [Resolved(CanBeNull = true)] + private IBindable? multiplayerRoomItem { get; set; } + + [Resolved] + private OsuGameBase game { get; set; } = null!; + /// /// Contains a dictionary with the current of all mods applicable for the current ruleset. /// @@ -92,6 +100,11 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool ShowPresets => false; + /// + /// Should overlay account for the multiplayer room global mods. + /// + public bool AccountForMultiplayerMods = false; + protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; @@ -278,7 +291,8 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - BeatmapInfo = { Value = beatmap?.BeatmapInfo } + BeatmapInfo = { Value = beatmap?.BeatmapInfo }, + AccountForMultiplayerMods = AccountForMultiplayerMods }, } }); @@ -332,6 +346,8 @@ namespace osu.Game.Overlays.Mods } }, true); + multiplayerRoomItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); + customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); SearchTextBox.Current.BindValueChanged(query => @@ -460,8 +476,20 @@ namespace osu.Game.Overlays.Mods foreach (var mod in SelectedMods.Value) multiplier *= mod.ScoreMultiplier; - rankingInformationDisplay.ModMultiplier.Value = multiplier; rankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); + + if (AccountForMultiplayerMods && multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + { + Ruleset ruleset = game.Ruleset.Value.CreateInstance(); + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + + foreach (var mod in multiplayerRoomMods) + multiplier *= mod.ScoreMultiplier; + + rankingInformationDisplay.Ranked.Value = rankingInformationDisplay.Ranked.Value && multiplayerRoomMods.All(m => m.Ranked); + } + + rankingInformationDisplay.ModMultiplier.Value = multiplier; } private void updateCustomisation() diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4c0219eff5..71f8bfa0f4 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -244,7 +244,8 @@ namespace osu.Game.Screens.OnlinePlay.Match LoadComponent(UserModsSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Plum) { SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false + IsValidMod = _ => false, + AccountForMultiplayerMods = true }); } From 2df5787dc7e46ea573bbbd4c1608cf18564029d0 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 18 Feb 2024 03:13:57 +0200 Subject: [PATCH 11/88] Packed changes into separate class --- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 58 ++++----- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 66 +++------- .../Mods/MultiplayerModSelectOverlay.cs | 118 ++++++++++++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 5 +- 4 files changed, 166 insertions(+), 81 deletions(-) create mode 100644 osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index da14382175..93e7e0ae9c 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -46,10 +46,7 @@ namespace osu.Game.Overlays.Mods public bool AccountForMultiplayerMods = false; [Resolved] - private Bindable> mods { get; set; } = null!; - - [Resolved(CanBeNull = true)] - private IBindable? multiplayerRoomItem { get; set; } + protected Bindable> Mods { get; private set; } = null!; public BindableBool Collapsed { get; } = new BindableBool(true); @@ -61,7 +58,7 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuGameBase game { get; set; } = null!; - private IBindable gameRuleset = null!; + protected IBindable GameRuleset = null!; private CancellationTokenSource? cancellationSource; private IBindable starDifficulty = null!; @@ -109,16 +106,14 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - mods.BindValueChanged(_ => + Mods.BindValueChanged(_ => { modSettingChangeTracker?.Dispose(); - modSettingChangeTracker = new ModSettingChangeTracker(mods.Value); + modSettingChangeTracker = new ModSettingChangeTracker(Mods.Value); modSettingChangeTracker.SettingChanged += _ => updateValues(); updateValues(); }, true); - multiplayerRoomItem?.BindValueChanged(_ => mods.TriggerChange()); - BeatmapInfo.BindValueChanged(_ => updateValues(), true); Collapsed.BindValueChanged(_ => @@ -128,8 +123,8 @@ namespace osu.Game.Overlays.Mods updateCollapsedState(); }); - gameRuleset = game.Ruleset.GetBoundCopy(); - gameRuleset.BindValueChanged(_ => updateValues()); + GameRuleset = game.Ruleset.GetBoundCopy(); + GameRuleset.BindValueChanged(_ => updateValues()); BeatmapInfo.BindValueChanged(_ => updateValues(), true); @@ -159,6 +154,23 @@ namespace osu.Game.Overlays.Mods LeftContent.AutoSizeDuration = Content.AutoSizeDuration = transition_duration; } + protected virtual double GetRate() + { + double rate = 1; + foreach (var mod in Mods.Value.OfType()) + rate = mod.ApplyToRate(0, rate); + return rate; + } + + protected virtual BeatmapDifficulty GetDifficulty() + { + BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value!.Difficulty); + + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToDifficulty(originalDifficulty); + + return originalDifficulty; + } private void updateValues() => Scheduler.AddOnce(() => { if (BeatmapInfo.Value == null) @@ -175,28 +187,10 @@ namespace osu.Game.Overlays.Mods starRatingDisplay.FinishTransforms(true); }); - Ruleset ruleset = gameRuleset.Value.CreateInstance(); - - double rate = 1; - foreach (var mod in mods.Value.OfType()) - rate = mod.ApplyToRate(0, rate); - - BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); - - foreach (var mod in mods.Value.OfType()) - mod.ApplyToDifficulty(originalDifficulty); - - if (AccountForMultiplayerMods && multiplayerRoomItem != null && multiplayerRoomItem.Value != null) - { - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - - foreach (var mod in multiplayerRoomMods.OfType()) - rate = mod.ApplyToRate(0, rate); - - foreach (var mod in multiplayerRoomMods.OfType()) - mod.ApplyToDifficulty(originalDifficulty); - } + double rate = GetRate(); + BeatmapDifficulty originalDifficulty = GetDifficulty(); + Ruleset ruleset = GameRuleset.Value.CreateInstance(); BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 0804624ed8..12719475a8 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -25,9 +25,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Online.Rooms; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -44,11 +42,6 @@ namespace osu.Game.Overlays.Mods [Cached] public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); - [Resolved(CanBeNull = true)] - private IBindable? multiplayerRoomItem { get; set; } - - [Resolved] - private OsuGameBase game { get; set; } = null!; /// /// Contains a dictionary with the current of all mods applicable for the current ruleset. @@ -100,11 +93,6 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool ShowPresets => false; - /// - /// Should overlay account for the multiplayer room global mods. - /// - public bool AccountForMultiplayerMods = false; - protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; @@ -138,8 +126,14 @@ namespace osu.Game.Overlays.Mods private DeselectAllModsButton deselectAllModsButton = null!; private Container aboveColumnsContent = null!; - private RankingInformationDisplay? rankingInformationDisplay; - private BeatmapAttributesDisplay? beatmapAttributesDisplay; + protected RankingInformationDisplay? RankingInformationDisplay; + protected BeatmapAttributesDisplay? BeatmapAttributesDisplay; + protected virtual BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new BeatmapAttributesDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BeatmapInfo = { Value = Beatmap?.BeatmapInfo } + }; protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } @@ -159,8 +153,8 @@ namespace osu.Game.Overlays.Mods if (beatmap == value) return; beatmap = value; - if (IsLoaded && beatmapAttributesDisplay != null) - beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; + if (IsLoaded && BeatmapAttributesDisplay != null) + BeatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; } } @@ -282,18 +276,12 @@ namespace osu.Game.Overlays.Mods }, Children = new Drawable[] { - rankingInformationDisplay = new RankingInformationDisplay + RankingInformationDisplay = new RankingInformationDisplay { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight }, - beatmapAttributesDisplay = new BeatmapAttributesDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BeatmapInfo = { Value = beatmap?.BeatmapInfo }, - AccountForMultiplayerMods = AccountForMultiplayerMods - }, + BeatmapAttributesDisplay = GetBeatmapAttributesDisplay } }); } @@ -329,7 +317,7 @@ namespace osu.Game.Overlays.Mods SelectedMods.BindValueChanged(_ => { - updateRankingInformation(); + UpdateRankingInformation(); updateFromExternalSelection(); updateCustomisation(); @@ -342,12 +330,10 @@ namespace osu.Game.Overlays.Mods // // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); - modSettingChangeTracker.SettingChanged += _ => updateRankingInformation(); + modSettingChangeTracker.SettingChanged += _ => UpdateRankingInformation(); } }, true); - multiplayerRoomItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); - customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); SearchTextBox.Current.BindValueChanged(query => @@ -371,7 +357,7 @@ namespace osu.Game.Overlays.Mods SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; - if (beatmapAttributesDisplay != null) + if (BeatmapAttributesDisplay != null) { float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X; @@ -383,7 +369,7 @@ namespace osu.Game.Overlays.Mods // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. if (Alpha == 1) - beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; + BeatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; footerContentFlow.LayoutDuration = 200; footerContentFlow.LayoutEasing = Easing.OutQuint; @@ -466,9 +452,9 @@ namespace osu.Game.Overlays.Mods modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } - private void updateRankingInformation() + protected virtual void UpdateRankingInformation() { - if (rankingInformationDisplay == null) + if (RankingInformationDisplay == null) return; double multiplier = 1.0; @@ -476,20 +462,8 @@ namespace osu.Game.Overlays.Mods foreach (var mod in SelectedMods.Value) multiplier *= mod.ScoreMultiplier; - rankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); - - if (AccountForMultiplayerMods && multiplayerRoomItem != null && multiplayerRoomItem.Value != null) - { - Ruleset ruleset = game.Ruleset.Value.CreateInstance(); - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - - foreach (var mod in multiplayerRoomMods) - multiplier *= mod.ScoreMultiplier; - - rankingInformationDisplay.Ranked.Value = rankingInformationDisplay.Ranked.Value && multiplayerRoomMods.All(m => m.Ranked); - } - - rankingInformationDisplay.ModMultiplier.Value = multiplier; + RankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); + RankingInformationDisplay.ModMultiplier.Value = multiplier; } private void updateCustomisation() diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs new file mode 100644 index 0000000000..ca1df31271 --- /dev/null +++ b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs @@ -0,0 +1,118 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets; +using osu.Game.Online.Rooms; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Overlays.Mods +{ + public partial class MultiplayerModSelectOverlay : UserModSelectOverlay + { + public MultiplayerModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Plum) + : base(colourScheme) + { + } + + [Resolved(CanBeNull = true)] + private IBindable? multiplayerRoomItem { get; set; } + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + protected override BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new MultiplayerBeatmapAttributesDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BeatmapInfo = { Value = Beatmap?.BeatmapInfo }, + AccountForMultiplayerMods = true + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + multiplayerRoomItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); + } + + protected override void UpdateRankingInformation() + { + if (RankingInformationDisplay == null) + return; + + double multiplier = 1.0; + + foreach (var mod in SelectedMods.Value) + multiplier *= mod.ScoreMultiplier; + + RankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); + + if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + { + Ruleset ruleset = game.Ruleset.Value.CreateInstance(); + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + + foreach (var mod in multiplayerRoomMods) + multiplier *= mod.ScoreMultiplier; + + RankingInformationDisplay.Ranked.Value = RankingInformationDisplay.Ranked.Value && multiplayerRoomMods.All(m => m.Ranked); + } + + RankingInformationDisplay.ModMultiplier.Value = multiplier; + } + } + + public partial class MultiplayerBeatmapAttributesDisplay : BeatmapAttributesDisplay + { + public MultiplayerBeatmapAttributesDisplay() + : base() + { + } + + [Resolved(CanBeNull = true)] + private IBindable? multiplayerRoomItem { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + multiplayerRoomItem?.BindValueChanged(_ => Mods.TriggerChange()); + } + + protected override double GetRate() + { + double rate = base.GetRate(); + Ruleset ruleset = GameRuleset.Value.CreateInstance(); + + if (AccountForMultiplayerMods && multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + { + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + + foreach (var mod in multiplayerRoomMods.OfType()) + rate = mod.ApplyToRate(0, rate); + } + + return rate; + } + + protected override BeatmapDifficulty GetDifficulty() + { + BeatmapDifficulty originalDifficulty = base.GetDifficulty(); + Ruleset ruleset = GameRuleset.Value.CreateInstance(); + + if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + { + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + + foreach (var mod in multiplayerRoomMods.OfType()) + mod.ApplyToDifficulty(originalDifficulty); + } + + return originalDifficulty; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 71f8bfa0f4..fbc06cce9e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -241,11 +241,10 @@ namespace osu.Game.Screens.OnlinePlay.Match } }; - LoadComponent(UserModsSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Plum) + LoadComponent(UserModsSelectOverlay = new MultiplayerModSelectOverlay() { SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false, - AccountForMultiplayerMods = true + IsValidMod = _ => false }); } From 6fb3192648f723af818322021ac5cd5cc5743ad7 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 18 Feb 2024 03:14:36 +0200 Subject: [PATCH 12/88] Update BeatmapAttributesDisplay.cs --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 93e7e0ae9c..849444fa32 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -40,11 +40,6 @@ namespace osu.Game.Overlays.Mods public Bindable BeatmapInfo { get; } = new Bindable(); - /// - /// Should attribute display account for the multiplayer room global mods. - /// - public bool AccountForMultiplayerMods = false; - [Resolved] protected Bindable> Mods { get; private set; } = null!; From 4aaf016ee0ad4ff8eaebbbd207d7617e3d60a14b Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 18 Feb 2024 03:15:53 +0200 Subject: [PATCH 13/88] quality improvements --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 2 -- osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs | 11 ++--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 849444fa32..7517a502c7 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -160,10 +160,8 @@ namespace osu.Game.Overlays.Mods protected virtual BeatmapDifficulty GetDifficulty() { BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value!.Difficulty); - foreach (var mod in Mods.Value.OfType()) mod.ApplyToDifficulty(originalDifficulty); - return originalDifficulty; } private void updateValues() => Scheduler.AddOnce(() => diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs index ca1df31271..ab20e9dc89 100644 --- a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs @@ -29,8 +29,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - BeatmapInfo = { Value = Beatmap?.BeatmapInfo }, - AccountForMultiplayerMods = true + BeatmapInfo = { Value = Beatmap?.BeatmapInfo } }; protected override void LoadComplete() @@ -87,15 +86,12 @@ namespace osu.Game.Overlays.Mods { double rate = base.GetRate(); Ruleset ruleset = GameRuleset.Value.CreateInstance(); - - if (AccountForMultiplayerMods && multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) { var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - foreach (var mod in multiplayerRoomMods.OfType()) rate = mod.ApplyToRate(0, rate); } - return rate; } @@ -103,15 +99,12 @@ namespace osu.Game.Overlays.Mods { BeatmapDifficulty originalDifficulty = base.GetDifficulty(); Ruleset ruleset = GameRuleset.Value.CreateInstance(); - if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) { var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - foreach (var mod in multiplayerRoomMods.OfType()) mod.ApplyToDifficulty(originalDifficulty); } - return originalDifficulty; } } From 9070e973d3bf7a54dae89a8acd14acf16cdfe69c Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 18 Feb 2024 03:16:18 +0200 Subject: [PATCH 14/88] Update BeatmapAttributesDisplay.cs --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 7517a502c7..08e124d8ac 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -19,7 +19,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Overlays.Mods From a6b63efe7d695354e4ddddb6a7143667fe56e646 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 18 Feb 2024 03:28:24 +0200 Subject: [PATCH 15/88] Fixed NVika code quality errors --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 1 + osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs | 8 ++++++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 08e124d8ac..f010725c8b 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -163,6 +163,7 @@ namespace osu.Game.Overlays.Mods mod.ApplyToDifficulty(originalDifficulty); return originalDifficulty; } + private void updateValues() => Scheduler.AddOnce(() => { if (BeatmapInfo.Value == null) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 12719475a8..e29e685ece 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -42,7 +42,6 @@ namespace osu.Game.Overlays.Mods [Cached] public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); - /// /// Contains a dictionary with the current of all mods applicable for the current ruleset. /// @@ -128,6 +127,7 @@ namespace osu.Game.Overlays.Mods private Container aboveColumnsContent = null!; protected RankingInformationDisplay? RankingInformationDisplay; protected BeatmapAttributesDisplay? BeatmapAttributesDisplay; + protected virtual BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new BeatmapAttributesDisplay { Anchor = Anchor.BottomRight, diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs index ab20e9dc89..b399b6b878 100644 --- a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs @@ -86,12 +86,14 @@ namespace osu.Game.Overlays.Mods { double rate = base.GetRate(); Ruleset ruleset = GameRuleset.Value.CreateInstance(); - if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + + if (multiplayerRoomItem?.Value != null) { var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); foreach (var mod in multiplayerRoomMods.OfType()) rate = mod.ApplyToRate(0, rate); } + return rate; } @@ -99,12 +101,14 @@ namespace osu.Game.Overlays.Mods { BeatmapDifficulty originalDifficulty = base.GetDifficulty(); Ruleset ruleset = GameRuleset.Value.CreateInstance(); - if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + + if (multiplayerRoomItem?.Value != null) { var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); foreach (var mod in multiplayerRoomMods.OfType()) mod.ApplyToDifficulty(originalDifficulty); } + return originalDifficulty; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index fbc06cce9e..b20760b114 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -241,7 +241,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } }; - LoadComponent(UserModsSelectOverlay = new MultiplayerModSelectOverlay() + LoadComponent(UserModsSelectOverlay = new MultiplayerModSelectOverlay { SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false From 24171bd02bf608fdee7b3c39dfecfba11968647e Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 18 Feb 2024 03:34:02 +0200 Subject: [PATCH 16/88] Update MultiplayerModSelectOverlay.cs --- osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs index b399b6b878..78978e25e7 100644 --- a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Mods RankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); - if (multiplayerRoomItem != null && multiplayerRoomItem.Value != null) + if (multiplayerRoomItem?.Value != null) { Ruleset ruleset = game.Ruleset.Value.CreateInstance(); var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); @@ -68,11 +68,6 @@ namespace osu.Game.Overlays.Mods public partial class MultiplayerBeatmapAttributesDisplay : BeatmapAttributesDisplay { - public MultiplayerBeatmapAttributesDisplay() - : base() - { - } - [Resolved(CanBeNull = true)] private IBindable? multiplayerRoomItem { get; set; } From 24e3fe79a4554b45b9176411402928c35214e172 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 19 Feb 2024 17:02:53 +0100 Subject: [PATCH 17/88] Log `GlobalStatistics` when exporting logs from settings --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index fe88413e6a..82cc952e53 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -10,6 +10,7 @@ using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Statistics; using osu.Game.Configuration; using osu.Game.Localisation; using osu.Game.Overlays.Notifications; @@ -107,6 +108,9 @@ namespace osu.Game.Overlays.Settings.Sections.General try { + GlobalStatistics.OutputToLog(); + Logger.Flush(); + var logStorage = Logger.Storage; using (var outStream = storage.CreateFileSafely(archive_filename)) From 54ef397d56c0499ceb568b2b867978a94b6b98bd Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 20 Feb 2024 12:57:28 +0200 Subject: [PATCH 18/88] Changed override method now it overrides mods getter instead of calculate fuctions --- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 31 +++------ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++-- .../Mods/MultiplayerModSelectOverlay.cs | 68 +++++++------------ 3 files changed, 41 insertions(+), 70 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index f010725c8b..21b3a7faa9 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -41,6 +41,7 @@ namespace osu.Game.Overlays.Mods [Resolved] protected Bindable> Mods { get; private set; } = null!; + protected virtual IEnumerable SelectedMods => Mods.Value; public BindableBool Collapsed { get; } = new BindableBool(true); @@ -148,22 +149,6 @@ namespace osu.Game.Overlays.Mods LeftContent.AutoSizeDuration = Content.AutoSizeDuration = transition_duration; } - protected virtual double GetRate() - { - double rate = 1; - foreach (var mod in Mods.Value.OfType()) - rate = mod.ApplyToRate(0, rate); - return rate; - } - - protected virtual BeatmapDifficulty GetDifficulty() - { - BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value!.Difficulty); - foreach (var mod in Mods.Value.OfType()) - mod.ApplyToDifficulty(originalDifficulty); - return originalDifficulty; - } - private void updateValues() => Scheduler.AddOnce(() => { if (BeatmapInfo.Value == null) @@ -180,14 +165,20 @@ namespace osu.Game.Overlays.Mods starRatingDisplay.FinishTransforms(true); }); - double rate = GetRate(); - BeatmapDifficulty originalDifficulty = GetDifficulty(); + double rate = 1; + foreach (var mod in SelectedMods.OfType()) + rate = mod.ApplyToRate(0, rate); + + bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; + + BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); + + foreach (var mod in SelectedMods.OfType()) + mod.ApplyToDifficulty(originalDifficulty); Ruleset ruleset = GameRuleset.Value.CreateInstance(); BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); - bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; - TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e29e685ece..f374828a4a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -114,6 +114,8 @@ namespace osu.Game.Overlays.Mods public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); + protected virtual IEnumerable AllSelectedMods => SelectedMods.Value; + private readonly BindableBool customisationVisible = new BindableBool(); private Bindable textSearchStartsActive = null!; @@ -317,7 +319,7 @@ namespace osu.Game.Overlays.Mods SelectedMods.BindValueChanged(_ => { - UpdateRankingInformation(); + updateRankingInformation(); updateFromExternalSelection(); updateCustomisation(); @@ -330,7 +332,7 @@ namespace osu.Game.Overlays.Mods // // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); - modSettingChangeTracker.SettingChanged += _ => UpdateRankingInformation(); + modSettingChangeTracker.SettingChanged += _ => updateRankingInformation(); } }, true); @@ -452,17 +454,17 @@ namespace osu.Game.Overlays.Mods modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } - protected virtual void UpdateRankingInformation() + private void updateRankingInformation() { if (RankingInformationDisplay == null) return; double multiplier = 1.0; - foreach (var mod in SelectedMods.Value) + foreach (var mod in AllSelectedMods) multiplier *= mod.ScoreMultiplier; - RankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); + RankingInformationDisplay.Ranked.Value = AllSelectedMods.All(m => m.Ranked); RankingInformationDisplay.ModMultiplier.Value = multiplier; } diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs index 78978e25e7..d546392650 100644 --- a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs @@ -1,13 +1,13 @@ // 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 System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets; using osu.Game.Online.Rooms; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods @@ -39,30 +39,21 @@ namespace osu.Game.Overlays.Mods multiplayerRoomItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); } - protected override void UpdateRankingInformation() + protected override IEnumerable AllSelectedMods { - if (RankingInformationDisplay == null) - return; - - double multiplier = 1.0; - - foreach (var mod in SelectedMods.Value) - multiplier *= mod.ScoreMultiplier; - - RankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked); - - if (multiplayerRoomItem?.Value != null) + get { - Ruleset ruleset = game.Ruleset.Value.CreateInstance(); - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + IEnumerable allMods = SelectedMods.Value; - foreach (var mod in multiplayerRoomMods) - multiplier *= mod.ScoreMultiplier; + if (multiplayerRoomItem?.Value != null) + { + Ruleset ruleset = game.Ruleset.Value.CreateInstance(); + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + allMods = allMods.Concat(multiplayerRoomMods); + } - RankingInformationDisplay.Ranked.Value = RankingInformationDisplay.Ranked.Value && multiplayerRoomMods.All(m => m.Ranked); + return allMods; } - - RankingInformationDisplay.ModMultiplier.Value = multiplier; } } @@ -77,34 +68,21 @@ namespace osu.Game.Overlays.Mods multiplayerRoomItem?.BindValueChanged(_ => Mods.TriggerChange()); } - protected override double GetRate() + protected override IEnumerable SelectedMods { - double rate = base.GetRate(); - Ruleset ruleset = GameRuleset.Value.CreateInstance(); - - if (multiplayerRoomItem?.Value != null) + get { - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - foreach (var mod in multiplayerRoomMods.OfType()) - rate = mod.ApplyToRate(0, rate); + IEnumerable selectedMods = Mods.Value; + + if (multiplayerRoomItem?.Value != null) + { + Ruleset ruleset = GameRuleset.Value.CreateInstance(); + var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + selectedMods = selectedMods.Concat(multiplayerRoomMods); + } + + return selectedMods; } - - return rate; - } - - protected override BeatmapDifficulty GetDifficulty() - { - BeatmapDifficulty originalDifficulty = base.GetDifficulty(); - Ruleset ruleset = GameRuleset.Value.CreateInstance(); - - if (multiplayerRoomItem?.Value != null) - { - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - foreach (var mod in multiplayerRoomMods.OfType()) - mod.ApplyToDifficulty(originalDifficulty); - } - - return originalDifficulty; } } } From a4288e7ecc8e3b631b8f8a2d43ddbc83c787f033 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 20 Feb 2024 13:00:59 +0200 Subject: [PATCH 19/88] removed unnecessary changes --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f374828a4a..5b3bafae45 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -127,8 +127,8 @@ namespace osu.Game.Overlays.Mods private DeselectAllModsButton deselectAllModsButton = null!; private Container aboveColumnsContent = null!; - protected RankingInformationDisplay? RankingInformationDisplay; - protected BeatmapAttributesDisplay? BeatmapAttributesDisplay; + private RankingInformationDisplay? rankingInformationDisplay; + private BeatmapAttributesDisplay? beatmapAttributesDisplay; protected virtual BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new BeatmapAttributesDisplay { @@ -155,8 +155,8 @@ namespace osu.Game.Overlays.Mods if (beatmap == value) return; beatmap = value; - if (IsLoaded && BeatmapAttributesDisplay != null) - BeatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; + if (IsLoaded && beatmapAttributesDisplay != null) + beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; } } @@ -278,12 +278,12 @@ namespace osu.Game.Overlays.Mods }, Children = new Drawable[] { - RankingInformationDisplay = new RankingInformationDisplay + rankingInformationDisplay = new RankingInformationDisplay { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight }, - BeatmapAttributesDisplay = GetBeatmapAttributesDisplay + beatmapAttributesDisplay = GetBeatmapAttributesDisplay } }); } @@ -359,7 +359,7 @@ namespace osu.Game.Overlays.Mods SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; - if (BeatmapAttributesDisplay != null) + if (beatmapAttributesDisplay != null) { float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X; @@ -371,7 +371,7 @@ namespace osu.Game.Overlays.Mods // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. if (Alpha == 1) - BeatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; + beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; footerContentFlow.LayoutDuration = 200; footerContentFlow.LayoutEasing = Easing.OutQuint; @@ -456,7 +456,7 @@ namespace osu.Game.Overlays.Mods private void updateRankingInformation() { - if (RankingInformationDisplay == null) + if (rankingInformationDisplay == null) return; double multiplier = 1.0; @@ -464,8 +464,8 @@ namespace osu.Game.Overlays.Mods foreach (var mod in AllSelectedMods) multiplier *= mod.ScoreMultiplier; - RankingInformationDisplay.Ranked.Value = AllSelectedMods.All(m => m.Ranked); - RankingInformationDisplay.ModMultiplier.Value = multiplier; + rankingInformationDisplay.Ranked.Value = AllSelectedMods.All(m => m.Ranked); + rankingInformationDisplay.ModMultiplier.Value = multiplier; } private void updateCustomisation() From 8199a49ee23f12e729a78658a6decd95df56fbc5 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 20 Feb 2024 13:11:19 +0200 Subject: [PATCH 20/88] minor look changes --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 1 + osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 21b3a7faa9..7c3c971d76 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -41,6 +41,7 @@ namespace osu.Game.Overlays.Mods [Resolved] protected Bindable> Mods { get; private set; } = null!; + protected virtual IEnumerable SelectedMods => Mods.Value; public BindableBool Collapsed { get; } = new BindableBool(true); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5b3bafae45..8a3b1954ed 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -464,8 +464,8 @@ namespace osu.Game.Overlays.Mods foreach (var mod in AllSelectedMods) multiplier *= mod.ScoreMultiplier; - rankingInformationDisplay.Ranked.Value = AllSelectedMods.All(m => m.Ranked); rankingInformationDisplay.ModMultiplier.Value = multiplier; + rankingInformationDisplay.Ranked.Value = AllSelectedMods.All(m => m.Ranked); } private void updateCustomisation() From ed028e8d260854fc800a3331bef952682d5e811f Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 20 Feb 2024 13:13:13 +0200 Subject: [PATCH 21/88] Update MultiplayerModSelectOverlay.cs --- osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs index d546392650..ee087bb149 100644 --- a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs @@ -35,7 +35,6 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - multiplayerRoomItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); } From 4a314a8e316273ff1f23b4003ea232d413b31ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 11:17:18 +0100 Subject: [PATCH 22/88] Namespacify data structures used in websocket communications --- osu.Game/Online/Chat/WebSocketChatClient.cs | 2 ++ .../Notifications/WebSocket/{ => Events}/NewChatMessageData.cs | 2 +- .../Notifications/WebSocket/{ => Requests}/EndChatRequest.cs | 2 +- .../Notifications/WebSocket/{ => Requests}/StartChatRequest.cs | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) rename osu.Game/Online/Notifications/WebSocket/{ => Events}/NewChatMessageData.cs (94%) rename osu.Game/Online/Notifications/WebSocket/{ => Requests}/EndChatRequest.cs (89%) rename osu.Game/Online/Notifications/WebSocket/{ => Requests}/StartChatRequest.cs (89%) diff --git a/osu.Game/Online/Chat/WebSocketChatClient.cs b/osu.Game/Online/Chat/WebSocketChatClient.cs index 8e1b501b25..37774a1f5d 100644 --- a/osu.Game/Online/Chat/WebSocketChatClient.cs +++ b/osu.Game/Online/Chat/WebSocketChatClient.cs @@ -13,6 +13,8 @@ using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Notifications.WebSocket; +using osu.Game.Online.Notifications.WebSocket.Events; +using osu.Game.Online.Notifications.WebSocket.Requests; namespace osu.Game.Online.Chat { diff --git a/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs b/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs similarity index 94% rename from osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs rename to osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs index 850fbd226b..ff9f5ee9f7 100644 --- a/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs +++ b/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -namespace osu.Game.Online.Notifications.WebSocket +namespace osu.Game.Online.Notifications.WebSocket.Events { /// /// A websocket message sent from the server when new messages arrive. diff --git a/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs b/osu.Game/Online/Notifications/WebSocket/Requests/EndChatRequest.cs similarity index 89% rename from osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs rename to osu.Game/Online/Notifications/WebSocket/Requests/EndChatRequest.cs index 7f67587f5d..9058fea815 100644 --- a/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs +++ b/osu.Game/Online/Notifications/WebSocket/Requests/EndChatRequest.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Notifications.WebSocket +namespace osu.Game.Online.Notifications.WebSocket.Requests { /// /// A websocket message notifying the server that the client no longer wants to receive chat messages. diff --git a/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs b/osu.Game/Online/Notifications/WebSocket/Requests/StartChatRequest.cs similarity index 89% rename from osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs rename to osu.Game/Online/Notifications/WebSocket/Requests/StartChatRequest.cs index 9dd69a7377..bc96415642 100644 --- a/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs +++ b/osu.Game/Online/Notifications/WebSocket/Requests/StartChatRequest.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Notifications.WebSocket +namespace osu.Game.Online.Notifications.WebSocket.Requests { /// /// A websocket message notifying the server that the client wants to receive chat messages. From 48bf9680e135d1ca528828d411047d338cc07c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 11:29:59 +0100 Subject: [PATCH 23/88] Add new structures for receiving new medal data --- .../Events/NewPrivateNotificationEvent.cs | 39 +++++++++++++++++++ .../WebSocket/Events/UserAchievementUnlock.cs | 34 ++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 osu.Game/Online/Notifications/WebSocket/Events/NewPrivateNotificationEvent.cs create mode 100644 osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs diff --git a/osu.Game/Online/Notifications/WebSocket/Events/NewPrivateNotificationEvent.cs b/osu.Game/Online/Notifications/WebSocket/Events/NewPrivateNotificationEvent.cs new file mode 100644 index 0000000000..1fc9636136 --- /dev/null +++ b/osu.Game/Online/Notifications/WebSocket/Events/NewPrivateNotificationEvent.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.Online.Notifications.WebSocket.Events +{ + /// + /// Reference: https://github.com/ppy/osu-web/blob/master/app/Events/NewPrivateNotificationEvent.php + /// + public class NewPrivateNotificationEvent + { + [JsonProperty("id")] + public ulong ID { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonProperty("object_type")] + public string ObjectType { get; set; } = string.Empty; + + [JsonProperty("object_id")] + public ulong ObjectId { get; set; } + + [JsonProperty("source_user_id")] + public uint SourceUserID { get; set; } + + [JsonProperty("is_read")] + public bool IsRead { get; set; } + + [JsonProperty("details")] + public JObject? Details { get; set; } + } +} diff --git a/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs b/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs new file mode 100644 index 0000000000..6c7c8af4f4 --- /dev/null +++ b/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.Notifications.WebSocket.Events +{ + /// + /// Reference: https://github.com/ppy/osu-web/blob/master/app/Jobs/Notifications/UserAchievementUnlock.php + /// + public class UserAchievementUnlock + { + [JsonProperty("achievement_id")] + public uint AchievementId { get; set; } + + [JsonProperty("achievement_mode")] + public ushort? AchievementMode { get; set; } + + [JsonProperty("cover_url")] + public string CoverUrl { get; set; } = string.Empty; + + [JsonProperty("slug")] + public string Slug { get; set; } = string.Empty; + + [JsonProperty("title")] + public string Title { get; set; } = string.Empty; + + [JsonProperty("description")] + public string Description { get; set; } = string.Empty; + + [JsonProperty("user_id")] + public uint UserId { get; set; } + } +} From 4911f5208b2ba6903aa3ec807ba246238bf501d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 12:03:12 +0100 Subject: [PATCH 24/88] Demote medal "overlay" to animation I need the actual overlay to be doing way more things (receiving the actual websocket events, queueing the medals for display, handling activation mode), so the pre-existing API design of the overlay just will not fly. --- osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs | 2 +- osu.Game/Overlays/{MedalOverlay.cs => MedalAnimation.cs} | 4 ++-- osu.Game/Overlays/MedalSplash/DrawableMedal.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game/Overlays/{MedalOverlay.cs => MedalAnimation.cs} (99%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index 71ed0a14a2..afd4427629 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep(@"display", () => { - LoadComponentAsync(new MedalOverlay(new Medal + LoadComponentAsync(new MedalAnimation(new Medal { Name = @"Animations", InternalName = @"all-intro-doubletime", diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalAnimation.cs similarity index 99% rename from osu.Game/Overlays/MedalOverlay.cs rename to osu.Game/Overlays/MedalAnimation.cs index eba35ec6f9..80c06be87c 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalAnimation.cs @@ -27,7 +27,7 @@ using osu.Framework.Utils; namespace osu.Game.Overlays { - public partial class MedalOverlay : FocusedOverlayContainer + public partial class MedalAnimation : VisibilityContainer { public const float DISC_SIZE = 400; @@ -45,7 +45,7 @@ namespace osu.Game.Overlays private readonly Container content; - public MedalOverlay(Medal medal) + public MedalAnimation(Medal medal) { this.medal = medal; RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index f4f6fd2bc1..2beed6645a 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.MedalSplash public DrawableMedal(Medal medal) { this.medal = medal; - Position = new Vector2(0f, MedalOverlay.DISC_SIZE / 2); + Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2); FillFlowContainer infoFlow; Children = new Drawable[] @@ -174,7 +174,7 @@ namespace osu.Game.Overlays.MedalSplash .ScaleTo(1); this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo); - this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 30, duration, Easing.OutExpo); + this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 30, duration, Easing.OutExpo); unlocked.FadeInFromZero(duration); break; @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.MedalSplash .ScaleTo(1); this.ScaleTo(scale_when_full, duration, Easing.OutExpo); - this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo); + this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 60, duration, Easing.OutExpo); unlocked.Show(); name.FadeInFromZero(duration + 100); description.FadeInFromZero(duration * 2); From 4f321e242bd7650c0bfe9afec4d3746188df3cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 12:05:25 +0100 Subject: [PATCH 25/88] Enable NRT in `OsuFocusedOverlayContainer` --- .../Containers/OsuFocusedOverlayContainer.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 162c4b6a59..16539a812d 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -20,10 +18,10 @@ namespace osu.Game.Graphics.Containers [Cached(typeof(IPreviewTrackOwner))] public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler { - private Sample samplePopIn; - private Sample samplePopOut; - protected virtual string PopInSampleName => "UI/overlay-pop-in"; - protected virtual string PopOutSampleName => "UI/overlay-pop-out"; + protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); + + protected virtual string PopInSampleName => @"UI/overlay-pop-in"; + protected virtual string PopOutSampleName => @"UI/overlay-pop-out"; protected virtual double PopInOutSampleBalance => 0; protected override bool BlockNonPositionalInput => true; @@ -34,19 +32,20 @@ namespace osu.Game.Graphics.Containers /// protected virtual bool DimMainContent => true; - [Resolved(CanBeNull = true)] - private IOverlayManager overlayManager { get; set; } + [Resolved] + private IOverlayManager? overlayManager { get; set; } [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } = null!; - protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); + private Sample? samplePopIn; + private Sample? samplePopOut; - [BackgroundDependencyLoader(true)] - private void load(AudioManager audio) + [BackgroundDependencyLoader] + private void load(AudioManager? audio) { - samplePopIn = audio.Samples.Get(PopInSampleName); - samplePopOut = audio.Samples.Get(PopOutSampleName); + samplePopIn = audio?.Samples.Get(PopInSampleName); + samplePopOut = audio?.Samples.Get(PopOutSampleName); } protected override void LoadComplete() From 2e5b61302ab2652b09ac8fe57e48d76315b0d85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 12:53:46 +0100 Subject: [PATCH 26/88] Implement basic medal display flow --- .../Visual/Gameplay/TestSceneMedalOverlay.cs | 45 +++++-- .../Containers/OsuFocusedOverlayContainer.cs | 11 +- osu.Game/Overlays/MedalAnimation.cs | 15 +-- osu.Game/Overlays/MedalOverlay.cs | 112 ++++++++++++++++++ 4 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Overlays/MedalOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index afd4427629..ead5c5b418 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -1,26 +1,51 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.Notifications.WebSocket; +using osu.Game.Online.Notifications.WebSocket.Events; using osu.Game.Overlays; -using osu.Game.Users; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public partial class TestSceneMedalOverlay : OsuTestScene + public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene { - public TestSceneMedalOverlay() + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + private MedalOverlay overlay = null!; + + [SetUpSteps] + public void SetUpSteps() { - AddStep(@"display", () => + AddStep("create overlay", () => Child = overlay = new MedalOverlay()); + } + + [Test] + public void TestBasicAward() + { + AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage { - LoadComponentAsync(new MedalAnimation(new Medal + Event = @"new", + Data = JObject.FromObject(new NewPrivateNotificationEvent { - Name = @"Animations", - InternalName = @"all-intro-doubletime", - Description = @"More complex than you think.", - }), Add); - }); + Name = @"user_achievement_unlock", + Details = JObject.FromObject(new UserAchievementUnlock + { + Title = "Time And A Half", + Description = "Having a right ol' time. One and a half of them, almost.", + Slug = @"all-intro-doubletime" + }) + }) + })); + AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2); + AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); } } } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 16539a812d..1945b2f0dd 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -20,8 +20,8 @@ namespace osu.Game.Graphics.Containers { protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); - protected virtual string PopInSampleName => @"UI/overlay-pop-in"; - protected virtual string PopOutSampleName => @"UI/overlay-pop-out"; + protected virtual string? PopInSampleName => @"UI/overlay-pop-in"; + protected virtual string? PopOutSampleName => @"UI/overlay-pop-out"; protected virtual double PopInOutSampleBalance => 0; protected override bool BlockNonPositionalInput => true; @@ -44,8 +44,11 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader] private void load(AudioManager? audio) { - samplePopIn = audio?.Samples.Get(PopInSampleName); - samplePopOut = audio?.Samples.Get(PopOutSampleName); + if (!string.IsNullOrEmpty(PopInSampleName)) + samplePopIn = audio?.Samples.Get(PopInSampleName); + + if (!string.IsNullOrEmpty(PopOutSampleName)) + samplePopOut = audio?.Samples.Get(PopOutSampleName); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/MedalAnimation.cs b/osu.Game/Overlays/MedalAnimation.cs index 80c06be87c..041929be67 100644 --- a/osu.Game/Overlays/MedalAnimation.cs +++ b/osu.Game/Overlays/MedalAnimation.cs @@ -18,11 +18,9 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Audio; using osu.Framework.Graphics.Textures; -using osuTK.Input; using osu.Framework.Graphics.Shapes; using System; using osu.Framework.Graphics.Effects; -using osu.Framework.Input.Events; using osu.Framework.Utils; namespace osu.Game.Overlays @@ -190,17 +188,6 @@ namespace osu.Game.Overlays particleContainer.Add(new MedalParticle(RNG.Next(0, 359))); } - protected override bool OnClick(ClickEvent e) - { - dismiss(); - return true; - } - - protected override void OnFocusLost(FocusLostEvent e) - { - if (e.CurrentState.Keyboard.Keys.IsPressed(Key.Escape)) dismiss(); - } - private const double initial_duration = 400; private const double step_duration = 900; @@ -256,7 +243,7 @@ namespace osu.Game.Overlays this.FadeOut(200); } - private void dismiss() + public void Dismiss() { if (drawableMedal.State != DisplayState.Full) { diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs new file mode 100644 index 0000000000..c3d7b4b9fc --- /dev/null +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; +using osu.Game.Online.API; +using osu.Game.Online.Notifications.WebSocket; +using osu.Game.Online.Notifications.WebSocket.Events; +using osu.Game.Users; + +namespace osu.Game.Overlays +{ + public partial class MedalOverlay : OsuFocusedOverlayContainer + { + protected override string? PopInSampleName => null; + protected override string? PopOutSampleName => null; + + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() + { + showingMedals = false; + this.FadeOut(); + } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private Container medalContainer = null!; + private bool showingMedals; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + api.NotificationsClient.MessageReceived += handleMedalMessages; + + Add(medalContainer = new Container + { + RelativeSizeAxes = Axes.Both + }); + } + + private void handleMedalMessages(SocketMessage obj) + { + if (obj.Event != @"new") + return; + + var data = obj.Data?.ToObject(); + if (data == null || data.Name != @"user_achievement_unlock") + return; + + var details = data.Details?.ToObject(); + if (details == null) + return; + + var medal = new Medal + { + Name = details.Title, + InternalName = details.Slug, + Description = details.Description, + }; + + Show(); + LoadComponentAsync(new MedalAnimation(medal), animation => + { + medalContainer.Add(animation); + showingMedals = true; + }); + } + + protected override void Update() + { + base.Update(); + + if (showingMedals && !medalContainer.Any()) + Hide(); + } + + protected override bool OnClick(ClickEvent e) + { + (medalContainer.FirstOrDefault(anim => anim.IsAlive) as MedalAnimation)?.Dismiss(); + return true; + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Back) + { + (medalContainer.FirstOrDefault(anim => anim.IsAlive) as MedalAnimation)?.Dismiss(); + return true; + } + + return base.OnPressed(e); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (api.IsNotNull()) + api.NotificationsClient.MessageReceived -= handleMedalMessages; + } + } +} From e4971ae121cf6078cf7e1150cf5176e26d093250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 13:08:02 +0100 Subject: [PATCH 27/88] Add display queueing when multiple medals are granted in quick succession --- .../Visual/Gameplay/TestSceneMedalOverlay.cs | 51 ++++++++++++++----- osu.Game/Overlays/MedalOverlay.cs | 12 ++++- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index ead5c5b418..a33c7e662f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -29,23 +29,48 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBasicAward() { - AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage + awardMedal(new UserAchievementUnlock { - Event = @"new", - Data = JObject.FromObject(new NewPrivateNotificationEvent - { - Name = @"user_achievement_unlock", - Details = JObject.FromObject(new UserAchievementUnlock - { - Title = "Time And A Half", - Description = "Having a right ol' time. One and a half of them, almost.", - Slug = @"all-intro-doubletime" - }) - }) - })); + Title = "Time And A Half", + Description = "Having a right ol' time. One and a half of them, almost.", + Slug = @"all-intro-doubletime" + }); AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible)); AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2); AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); } + + [Test] + public void TestMultipleMedalsInQuickSuccession() + { + awardMedal(new UserAchievementUnlock + { + Title = "Time And A Half", + Description = "Having a right ol' time. One and a half of them, almost.", + Slug = @"all-intro-doubletime" + }); + awardMedal(new UserAchievementUnlock + { + Title = "S-Ranker", + Description = "Accuracy is really underrated.", + Slug = @"all-secret-rank-s" + }); + awardMedal(new UserAchievementUnlock + { + Title = "500 Combo", + Description = "500 big ones! You're moving up in the world!", + Slug = @"osu-combo-500" + }); + } + + private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage + { + Event = @"new", + Data = JObject.FromObject(new NewPrivateNotificationEvent + { + Name = @"user_achievement_unlock", + Details = JObject.FromObject(unlock) + }) + })); } } diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index c3d7b4b9fc..70cde43924 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -1,6 +1,7 @@ // 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 System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -29,6 +30,8 @@ namespace osu.Game.Overlays this.FadeOut(); } + private readonly Queue queuedMedals = new Queue(); + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -71,7 +74,7 @@ namespace osu.Game.Overlays Show(); LoadComponentAsync(new MedalAnimation(medal), animation => { - medalContainer.Add(animation); + queuedMedals.Enqueue(animation); showingMedals = true; }); } @@ -80,7 +83,12 @@ namespace osu.Game.Overlays { base.Update(); - if (showingMedals && !medalContainer.Any()) + if (!showingMedals || medalContainer.Any()) + return; + + if (queuedMedals.TryDequeue(out var nextMedal)) + medalContainer.Add(nextMedal); + else Hide(); } From b334b78b636a7d4504f3509c88fbf2542d8f4f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 13:44:25 +0100 Subject: [PATCH 28/88] Make medal overlay respect overlay disable via activation mode --- .../Visual/Gameplay/TestSceneMedalOverlay.cs | 35 ++++++++++++++++-- osu.Game/Overlays/MedalOverlay.cs | 36 +++++++++++-------- osu.Game/Properties/AssemblyInfo.cs | 3 ++ 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index a33c7e662f..fe9c524285 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -1,8 +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 Moq; using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API; @@ -16,14 +19,26 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene { - private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All); + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private MedalOverlay overlay = null!; [SetUpSteps] public void SetUpSteps() { - AddStep("create overlay", () => Child = overlay = new MedalOverlay()); + var overlayManagerMock = new Mock(); + overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode); + + AddStep("create overlay", () => Child = new DependencyProvidingContainer + { + Child = overlay = new MedalOverlay(), + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(IOverlayManager), overlayManagerMock.Object) + ] + }); } [Test] @@ -63,6 +78,22 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [Test] + public void TestDelayMedalDisplayUntilActivationModeAllowsIt() + { + AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled); + awardMedal(new UserAchievementUnlock + { + Title = "Time And A Half", + Description = "Having a right ol' time. One and a half of them, almost.", + Slug = @"all-intro-doubletime" + }); + AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); + + AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All); + AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + } + private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage { Event = @"new", diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 70cde43924..03beba2d3b 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -24,11 +24,7 @@ namespace osu.Game.Overlays protected override void PopIn() => this.FadeIn(); - protected override void PopOut() - { - showingMedals = false; - this.FadeOut(); - } + protected override void PopOut() => this.FadeOut(); private readonly Queue queuedMedals = new Queue(); @@ -36,7 +32,6 @@ namespace osu.Game.Overlays private IAPIProvider api { get; set; } = null!; private Container medalContainer = null!; - private bool showingMedals; [BackgroundDependencyLoader] private void load() @@ -51,6 +46,17 @@ namespace osu.Game.Overlays }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + OverlayActivationMode.BindValueChanged(val => + { + if (val.NewValue != OverlayActivation.Disabled && queuedMedals.Any()) + Show(); + }, true); + } + private void handleMedalMessages(SocketMessage obj) { if (obj.Event != @"new") @@ -71,25 +77,25 @@ namespace osu.Game.Overlays Description = details.Description, }; + var medalAnimation = new MedalAnimation(medal); + queuedMedals.Enqueue(medalAnimation); Show(); - LoadComponentAsync(new MedalAnimation(medal), animation => - { - queuedMedals.Enqueue(animation); - showingMedals = true; - }); } protected override void Update() { base.Update(); - if (!showingMedals || medalContainer.Any()) + if (medalContainer.Any()) return; - if (queuedMedals.TryDequeue(out var nextMedal)) - medalContainer.Add(nextMedal); - else + if (!queuedMedals.TryDequeue(out var medal)) + { Hide(); + return; + } + + LoadComponentAsync(medal, medalContainer.Add); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Properties/AssemblyInfo.cs b/osu.Game/Properties/AssemblyInfo.cs index 1b77e45891..be430a0fe4 100644 --- a/osu.Game/Properties/AssemblyInfo.cs +++ b/osu.Game/Properties/AssemblyInfo.cs @@ -11,3 +11,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")] [assembly: InternalsVisibleTo("osu.Game.Tests.iOS")] [assembly: InternalsVisibleTo("osu.Game.Tests.Android")] + +// intended for Moq usage +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] From 8abcc70b938c1ba6d23c1d61ac15cf7cad31b02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 14:33:08 +0100 Subject: [PATCH 29/88] Add medal overlay to game --- .../Navigation/TestSceneScreenNavigation.cs | 25 +++++++++++++++++++ osu.Game/OsuGame.cs | 1 + 2 files changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 7e42d4781d..0fa2fd4b0b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -6,6 +6,7 @@ using System; using System.IO; using System.Linq; +using Newtonsoft.Json.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; +using osu.Game.Online.Notifications.WebSocket; +using osu.Game.Online.Notifications.WebSocket.Events; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; @@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); } + [Test] + public void TestShowMedalAtResults() + { + playToResults(); + + AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage + { + Event = @"new", + Data = JObject.FromObject(new NewPrivateNotificationEvent + { + Name = @"user_achievement_unlock", + Details = JObject.FromObject(new UserAchievementUnlock + { + Title = "Time And A Half", + Description = "Having a right ol' time. One and a half of them, almost.", + Slug = @"all-intro-doubletime" + }) + }) + })); + AddUntilStep("medal overlay shown", () => Game.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + } + [Test] public void TestRetryFromResults() { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 9ffa88947b..a829758f0e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1072,6 +1072,7 @@ namespace osu.Game loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); + loadComponentSingleFile(new MedalOverlay(), overlayContent.Add); loadComponentSingleFile(new LoginOverlay { From 96825915f73c558265f736472e95d7e6c68fa19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 15:02:30 +0100 Subject: [PATCH 30/88] Fix threading failure Implicitly showing the medal overlay fires off some transforms, and the websocket listener runs on a TPL thread. That's a recipe for disaster, so schedule the show call. --- osu.Game/Overlays/MedalOverlay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 03beba2d3b..76936e0f5a 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -22,6 +22,8 @@ namespace osu.Game.Overlays protected override string? PopInSampleName => null; protected override string? PopOutSampleName => null; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + protected override void PopIn() => this.FadeIn(); protected override void PopOut() => this.FadeOut(); @@ -52,7 +54,7 @@ namespace osu.Game.Overlays OverlayActivationMode.BindValueChanged(val => { - if (val.NewValue != OverlayActivation.Disabled && queuedMedals.Any()) + if (val.NewValue != OverlayActivation.Disabled && (queuedMedals.Any() || medalContainer.Any())) Show(); }, true); } @@ -79,7 +81,7 @@ namespace osu.Game.Overlays var medalAnimation = new MedalAnimation(medal); queuedMedals.Enqueue(medalAnimation); - Show(); + Scheduler.AddOnce(Show); } protected override void Update() From 611e3fe76bd0930e0ef0320c57ba23658e9b2bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 15:52:15 +0100 Subject: [PATCH 31/88] Delay medal display when wanting to show results animation --- osu.Game/Overlays/MedalOverlay.cs | 5 +++-- .../Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 5 +++++ osu.Game/Screens/Ranking/ResultsScreen.cs | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 76936e0f5a..3601566dda 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays OverlayActivationMode.BindValueChanged(val => { - if (val.NewValue != OverlayActivation.Disabled && (queuedMedals.Any() || medalContainer.Any())) + if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any())) Show(); }, true); } @@ -81,7 +81,8 @@ namespace osu.Game.Overlays var medalAnimation = new MedalAnimation(medal); queuedMedals.Enqueue(medalAnimation); - Scheduler.AddOnce(Show); + if (OverlayActivationMode.Value == OverlayActivation.All) + Scheduler.AddOnce(Show); } protected override void Update() diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index d209c305fa..f807126614 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -31,6 +31,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// public partial class AccuracyCircle : CompositeDrawable { + /// + /// The total duration of the animation. + /// + public const double TOTAL_DURATION = APPEAR_DURATION + ACCURACY_TRANSFORM_DELAY + ACCURACY_TRANSFORM_DURATION; + /// /// Duration for the transforms causing this component to appear. /// diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 69cfbed8f2..93114b1d18 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -25,8 +25,10 @@ using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Placeholders; +using osu.Game.Overlays; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -41,6 +43,8 @@ namespace osu.Game.Screens.Ranking public override bool? AllowGlobalTrackControl => true; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; + public readonly Bindable SelectedScore = new Bindable(); [CanBeNull] @@ -160,6 +164,10 @@ namespace osu.Game.Screens.Ranking bool shouldFlair = player != null && !Score.User.IsBot; ScorePanelList.AddScore(Score, shouldFlair); + // this is mostly for medal display. + // we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode` + // to give the results screen enough time to play the animation out before the medals can be shown. + Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0); } if (allowWatchingReplay) From 1db5cd3abadcc5f24ceada0092fcf8e2d7ece869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 16:16:30 +0100 Subject: [PATCH 32/88] Move medal overlay to topmost container --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a829758f0e..dbdc86e016 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1072,7 +1072,6 @@ namespace osu.Game loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true); - loadComponentSingleFile(new MedalOverlay(), overlayContent.Add); loadComponentSingleFile(new LoginOverlay { @@ -1088,6 +1087,7 @@ namespace osu.Game loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); + loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add); loadComponentSingleFile(CreateHighPerformanceSession(), Add); From 9b938f333de39b0d497cab545f4e0719e17c9b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 17:25:11 +0100 Subject: [PATCH 33/88] Fix test failures --- osu.Game/Overlays/MedalOverlay.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 3601566dda..072d7db6c7 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -34,6 +34,7 @@ namespace osu.Game.Overlays private IAPIProvider api { get; set; } = null!; private Container medalContainer = null!; + private MedalAnimation? lastAnimation; [BackgroundDependencyLoader] private void load() @@ -54,7 +55,7 @@ namespace osu.Game.Overlays OverlayActivationMode.BindValueChanged(val => { - if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any())) + if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false)) Show(); }, true); } @@ -89,21 +90,21 @@ namespace osu.Game.Overlays { base.Update(); - if (medalContainer.Any()) + if (medalContainer.Any() || lastAnimation?.IsLoaded == false) return; - if (!queuedMedals.TryDequeue(out var medal)) + if (!queuedMedals.TryDequeue(out lastAnimation)) { Hide(); return; } - LoadComponentAsync(medal, medalContainer.Add); + LoadComponentAsync(lastAnimation, medalContainer.Add); } protected override bool OnClick(ClickEvent e) { - (medalContainer.FirstOrDefault(anim => anim.IsAlive) as MedalAnimation)?.Dismiss(); + lastAnimation?.Dismiss(); return true; } @@ -111,7 +112,7 @@ namespace osu.Game.Overlays { if (e.Action == GlobalAction.Back) { - (medalContainer.FirstOrDefault(anim => anim.IsAlive) as MedalAnimation)?.Dismiss(); + lastAnimation?.Dismiss(); return true; } From 33a0dcaf20fad0c79802367f81b9a370a9be035f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 17:59:21 +0100 Subject: [PATCH 34/88] NRT-annotate `MedalAnimation` and fix possible nullref --- osu.Game/Overlays/MedalAnimation.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/MedalAnimation.cs b/osu.Game/Overlays/MedalAnimation.cs index 041929be67..25776d50db 100644 --- a/osu.Game/Overlays/MedalAnimation.cs +++ b/osu.Game/Overlays/MedalAnimation.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; @@ -20,6 +18,7 @@ using osu.Framework.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Shapes; using System; +using System.Diagnostics; using osu.Framework.Graphics.Effects; using osu.Framework.Utils; @@ -37,9 +36,9 @@ namespace osu.Game.Overlays private readonly BackgroundStrip leftStrip, rightStrip; private readonly CircularContainer disc; private readonly Sprite innerSpin, outerSpin; - private DrawableMedal drawableMedal; - private Sample getSample; + private DrawableMedal? drawableMedal; + private Sample? getSample; private readonly Container content; @@ -197,7 +196,7 @@ namespace osu.Game.Overlays background.FlashColour(Color4.White.Opacity(0.25f), 400); - getSample.Play(); + getSample?.Play(); innerSpin.Spin(20000, RotationDirection.Clockwise); outerSpin.Spin(40000, RotationDirection.Clockwise); @@ -216,6 +215,8 @@ namespace osu.Game.Overlays leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint); rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint); + Debug.Assert(drawableMedal != null); + this.Animate().Schedule(() => { if (drawableMedal.State != DisplayState.Full) @@ -245,7 +246,7 @@ namespace osu.Game.Overlays public void Dismiss() { - if (drawableMedal.State != DisplayState.Full) + if (drawableMedal != null && drawableMedal.State != DisplayState.Full) { // if we haven't yet, play out the animation fully drawableMedal.State = DisplayState.Full; From e5be8838e68e10cc11c14dc11f5ee1dbfb5a69eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Feb 2024 18:42:01 +0100 Subject: [PATCH 35/88] Fix yet another failure --- osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index fe9c524285..5dc553b9df 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using Moq; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -51,6 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay Slug = @"all-intro-doubletime" }); AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("wait for load", () => this.ChildrenOfType().Any()); AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2); AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); } From ae9c58be3030cb5c0b3c0148446afbf8edf452ac Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 16:42:58 +0300 Subject: [PATCH 36/88] Remove "multiplayer" references from subclass and move to appropriate place --- .../OnlinePlay/Match/RoomModSelectOverlay.cs} | 24 ++++++++++--------- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) rename osu.Game/{Overlays/Mods/MultiplayerModSelectOverlay.cs => Screens/OnlinePlay/Match/RoomModSelectOverlay.cs} (72%) diff --git a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs similarity index 72% rename from osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs rename to osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index ee087bb149..00704b7ec7 100644 --- a/osu.Game/Overlays/Mods/MultiplayerModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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; @@ -6,26 +6,28 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -namespace osu.Game.Overlays.Mods +namespace osu.Game.Screens.OnlinePlay.Match { - public partial class MultiplayerModSelectOverlay : UserModSelectOverlay + public partial class RoomModSelectOverlay : UserModSelectOverlay { - public MultiplayerModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Plum) + public RoomModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Plum) : base(colourScheme) { } [Resolved(CanBeNull = true)] - private IBindable? multiplayerRoomItem { get; set; } + private IBindable? selectedItem { get; set; } [Resolved] private OsuGameBase game { get; set; } = null!; - protected override BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new MultiplayerBeatmapAttributesDisplay + protected override BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new RoomBeatmapAttributesDisplay { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -35,7 +37,7 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - multiplayerRoomItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); + selectedItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); } protected override IEnumerable AllSelectedMods @@ -44,10 +46,10 @@ namespace osu.Game.Overlays.Mods { IEnumerable allMods = SelectedMods.Value; - if (multiplayerRoomItem?.Value != null) + if (selectedItem?.Value != null) { Ruleset ruleset = game.Ruleset.Value.CreateInstance(); - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); + var multiplayerRoomMods = selectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); allMods = allMods.Concat(multiplayerRoomMods); } @@ -56,7 +58,7 @@ namespace osu.Game.Overlays.Mods } } - public partial class MultiplayerBeatmapAttributesDisplay : BeatmapAttributesDisplay + public partial class RoomBeatmapAttributesDisplay : BeatmapAttributesDisplay { [Resolved(CanBeNull = true)] private IBindable? multiplayerRoomItem { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index b20760b114..97fbb83992 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -241,7 +241,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } }; - LoadComponent(UserModsSelectOverlay = new MultiplayerModSelectOverlay + LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay { SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false From f94cd4483cb4f4b0a4b8cd189975ccca92f54720 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 16:47:37 +0300 Subject: [PATCH 37/88] Avoid relying on game-wide ruleset bindable --- .../OnlinePlay/Match/RoomModSelectOverlay.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index 00704b7ec7..e6ff2260e9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -25,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable? selectedItem { get; set; } [Resolved] - private OsuGameBase game { get; set; } = null!; + private RulesetStore rulesets { get; set; } = null!; protected override BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new RoomBeatmapAttributesDisplay { @@ -48,9 +49,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (selectedItem?.Value != null) { - Ruleset ruleset = game.Ruleset.Value.CreateInstance(); - var multiplayerRoomMods = selectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - allMods = allMods.Concat(multiplayerRoomMods); + var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + allMods = allMods.Concat(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); } return allMods; @@ -61,12 +62,15 @@ namespace osu.Game.Screens.OnlinePlay.Match public partial class RoomBeatmapAttributesDisplay : BeatmapAttributesDisplay { [Resolved(CanBeNull = true)] - private IBindable? multiplayerRoomItem { get; set; } + private IBindable? selectedItem { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; protected override void LoadComplete() { base.LoadComplete(); - multiplayerRoomItem?.BindValueChanged(_ => Mods.TriggerChange()); + selectedItem?.BindValueChanged(_ => Mods.TriggerChange()); } protected override IEnumerable SelectedMods @@ -75,11 +79,11 @@ namespace osu.Game.Screens.OnlinePlay.Match { IEnumerable selectedMods = Mods.Value; - if (multiplayerRoomItem?.Value != null) + if (selectedItem?.Value != null) { - Ruleset ruleset = GameRuleset.Value.CreateInstance(); - var multiplayerRoomMods = multiplayerRoomItem.Value.RequiredMods.Select(m => m.ToMod(ruleset)); - selectedMods = selectedMods.Concat(multiplayerRoomMods); + var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + selectedMods = selectedMods.Concat(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); } return selectedMods; From 918577d530c0fa27fc58f2677a731df23b20d230 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 16:51:52 +0300 Subject: [PATCH 38/88] Compute required mods list once per update --- .../OnlinePlay/Match/RoomModSelectOverlay.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index e6ff2260e9..5406912f49 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -35,28 +35,28 @@ namespace osu.Game.Screens.OnlinePlay.Match BeatmapInfo = { Value = Beatmap?.BeatmapInfo } }; + private readonly List roomMods = new List(); + protected override void LoadComplete() { base.LoadComplete(); - selectedItem?.BindValueChanged(_ => SelectedMods.TriggerChange()); - } - protected override IEnumerable AllSelectedMods - { - get + selectedItem?.BindValueChanged(_ => { - IEnumerable allMods = SelectedMods.Value; + roomMods.Clear(); if (selectedItem?.Value != null) { var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - allMods = allMods.Concat(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); + roomMods.AddRange(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); } - return allMods; - } + SelectedMods.TriggerChange(); + }); } + + protected override IEnumerable AllSelectedMods => roomMods.Concat(base.AllSelectedMods); } public partial class RoomBeatmapAttributesDisplay : BeatmapAttributesDisplay @@ -67,27 +67,27 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } = null!; + private readonly List roomMods = new List(); + protected override void LoadComplete() { base.LoadComplete(); - selectedItem?.BindValueChanged(_ => Mods.TriggerChange()); - } - protected override IEnumerable SelectedMods - { - get + selectedItem?.BindValueChanged(_ => { - IEnumerable selectedMods = Mods.Value; + roomMods.Clear(); if (selectedItem?.Value != null) { var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - selectedMods = selectedMods.Concat(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); + roomMods.AddRange(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); } - return selectedMods; - } + Mods.TriggerChange(); + }); } + + protected override IEnumerable SelectedMods => roomMods.Concat(base.SelectedMods); } } From 323d7f8e2dabbd090c29b51926338cece1a94f64 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 16:59:08 +0300 Subject: [PATCH 39/88] Change `BeatmapAttributesDisplay` retrieval to proper method --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 16 +++--- .../OnlinePlay/Match/RoomModSelectOverlay.cs | 56 +++++++++---------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8a3b1954ed..99981c28d5 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -130,13 +130,6 @@ namespace osu.Game.Overlays.Mods private RankingInformationDisplay? rankingInformationDisplay; private BeatmapAttributesDisplay? beatmapAttributesDisplay; - protected virtual BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new BeatmapAttributesDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BeatmapInfo = { Value = Beatmap?.BeatmapInfo } - }; - protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } protected SelectAllModsButton? SelectAllModsButton { get; set; } @@ -283,7 +276,12 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight }, - beatmapAttributesDisplay = GetBeatmapAttributesDisplay + beatmapAttributesDisplay = CreateBeatmapAttributesDisplay().With(b => + { + b.Anchor = Anchor.BottomRight; + b.Origin = Anchor.BottomRight; + b.BeatmapInfo.Value = Beatmap?.BeatmapInfo; + }), } }); } @@ -293,6 +291,8 @@ namespace osu.Game.Overlays.Mods textSearchStartsActive = configManager.GetBindable(OsuSetting.ModSelectTextSearchStartsActive); } + protected virtual BeatmapAttributesDisplay CreateBeatmapAttributesDisplay() => new BeatmapAttributesDisplay(); + public override void Hide() { base.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index 5406912f49..4bf8e22d4e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -28,13 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } = null!; - protected override BeatmapAttributesDisplay GetBeatmapAttributesDisplay => new RoomBeatmapAttributesDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BeatmapInfo = { Value = Beatmap?.BeatmapInfo } - }; - private readonly List roomMods = new List(); protected override void LoadComplete() @@ -57,37 +49,39 @@ namespace osu.Game.Screens.OnlinePlay.Match } protected override IEnumerable AllSelectedMods => roomMods.Concat(base.AllSelectedMods); - } - public partial class RoomBeatmapAttributesDisplay : BeatmapAttributesDisplay - { - [Resolved(CanBeNull = true)] - private IBindable? selectedItem { get; set; } + protected override BeatmapAttributesDisplay CreateBeatmapAttributesDisplay() => new RoomBeatmapAttributesDisplay(); - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - private readonly List roomMods = new List(); - - protected override void LoadComplete() + private partial class RoomBeatmapAttributesDisplay : BeatmapAttributesDisplay { - base.LoadComplete(); + [Resolved(CanBeNull = true)] + private IBindable? selectedItem { get; set; } - selectedItem?.BindValueChanged(_ => + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private readonly List roomMods = new List(); + + protected override void LoadComplete() { - roomMods.Clear(); + base.LoadComplete(); - if (selectedItem?.Value != null) + selectedItem?.BindValueChanged(_ => { - var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - roomMods.AddRange(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); - } + roomMods.Clear(); - Mods.TriggerChange(); - }); + if (selectedItem?.Value != null) + { + var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + roomMods.AddRange(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); + } + + Mods.TriggerChange(); + }); + } + + protected override IEnumerable SelectedMods => roomMods.Concat(base.SelectedMods); } - - protected override IEnumerable SelectedMods => roomMods.Concat(base.SelectedMods); } } From 9ce07a96b2f98358050eacb4d72767fc1dede2cc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 17:28:54 +0300 Subject: [PATCH 40/88] Rewrite mods flow and remove `RoomBeatmapAttributesDisplay` --- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 26 ++++--------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 34 +++++++++++------ .../OnlinePlay/Match/RoomModSelectOverlay.cs | 37 +------------------ 3 files changed, 31 insertions(+), 66 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 7c3c971d76..8f84b51127 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -13,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -39,15 +38,10 @@ namespace osu.Game.Overlays.Mods public Bindable BeatmapInfo { get; } = new Bindable(); - [Resolved] - protected Bindable> Mods { get; private set; } = null!; - - protected virtual IEnumerable SelectedMods => Mods.Value; + public Bindable> Mods { get; } = new Bindable>(); public BindableBool Collapsed { get; } = new BindableBool(true); - private ModSettingChangeTracker? modSettingChangeTracker; - [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; @@ -102,15 +96,8 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - Mods.BindValueChanged(_ => - { - modSettingChangeTracker?.Dispose(); - modSettingChangeTracker = new ModSettingChangeTracker(Mods.Value); - modSettingChangeTracker.SettingChanged += _ => updateValues(); - updateValues(); - }, true); - - BeatmapInfo.BindValueChanged(_ => updateValues(), true); + Mods.BindValueChanged(_ => updateValues()); + BeatmapInfo.BindValueChanged(_ => updateValues()); Collapsed.BindValueChanged(_ => { @@ -122,8 +109,9 @@ namespace osu.Game.Overlays.Mods GameRuleset = game.Ruleset.GetBoundCopy(); GameRuleset.BindValueChanged(_ => updateValues()); - BeatmapInfo.BindValueChanged(_ => updateValues(), true); + BeatmapInfo.BindValueChanged(_ => updateValues()); + updateValues(); updateCollapsedState(); } @@ -167,14 +155,14 @@ namespace osu.Game.Overlays.Mods }); double rate = 1; - foreach (var mod in SelectedMods.OfType()) + foreach (var mod in Mods.Value.OfType()) rate = mod.ApplyToRate(0, rate); bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); - foreach (var mod in SelectedMods.OfType()) + foreach (var mod in Mods.Value.OfType()) mod.ApplyToDifficulty(originalDifficulty); Ruleset ruleset = GameRuleset.Value.CreateInstance(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 99981c28d5..fa87b0aa0d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -114,8 +114,6 @@ namespace osu.Game.Overlays.Mods public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); - protected virtual IEnumerable AllSelectedMods => SelectedMods.Value; - private readonly BindableBool customisationVisible = new BindableBool(); private Bindable textSearchStartsActive = null!; @@ -319,7 +317,7 @@ namespace osu.Game.Overlays.Mods SelectedMods.BindValueChanged(_ => { - updateRankingInformation(); + UpdateOverlayInformation(SelectedMods.Value); updateFromExternalSelection(); updateCustomisation(); @@ -332,7 +330,7 @@ namespace osu.Game.Overlays.Mods // // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); - modSettingChangeTracker.SettingChanged += _ => updateRankingInformation(); + modSettingChangeTracker.SettingChanged += _ => UpdateOverlayInformation(SelectedMods.Value); } }, true); @@ -454,18 +452,30 @@ namespace osu.Game.Overlays.Mods modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } - private void updateRankingInformation() + /// + /// Updates any information displayed on the overlay regarding the effects of the selected mods. + /// + /// The list of mods to show effect from. This can be overriden to include effect of mods that are not part of the bindable (e.g. room mods in multiplayer/playlists). + protected virtual void UpdateOverlayInformation(IReadOnlyList mods) { - if (rankingInformationDisplay == null) - return; + if (rankingInformationDisplay != null) + { + double multiplier = 1.0; - double multiplier = 1.0; + foreach (var mod in mods) + multiplier *= mod.ScoreMultiplier; - foreach (var mod in AllSelectedMods) - multiplier *= mod.ScoreMultiplier; + rankingInformationDisplay.ModMultiplier.Value = multiplier; + rankingInformationDisplay.Ranked.Value = mods.All(m => m.Ranked); + } - rankingInformationDisplay.ModMultiplier.Value = multiplier; - rankingInformationDisplay.Ranked.Value = AllSelectedMods.All(m => m.Ranked); + if (beatmapAttributesDisplay != null) + { + if (!ReferenceEquals(beatmapAttributesDisplay.Mods.Value, mods)) + beatmapAttributesDisplay.Mods.Value = mods; + else + beatmapAttributesDisplay.Mods.TriggerChange(); // mods list may be same but a mod setting has changed, trigger change in that case. + } } private void updateCustomisation() diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index 4bf8e22d4e..dd0ecbad90 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -48,40 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Match }); } - protected override IEnumerable AllSelectedMods => roomMods.Concat(base.AllSelectedMods); - - protected override BeatmapAttributesDisplay CreateBeatmapAttributesDisplay() => new RoomBeatmapAttributesDisplay(); - - private partial class RoomBeatmapAttributesDisplay : BeatmapAttributesDisplay - { - [Resolved(CanBeNull = true)] - private IBindable? selectedItem { get; set; } - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - private readonly List roomMods = new List(); - - protected override void LoadComplete() - { - base.LoadComplete(); - - selectedItem?.BindValueChanged(_ => - { - roomMods.Clear(); - - if (selectedItem?.Value != null) - { - var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - roomMods.AddRange(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); - } - - Mods.TriggerChange(); - }); - } - - protected override IEnumerable SelectedMods => roomMods.Concat(base.SelectedMods); - } + protected override void UpdateOverlayInformation(IReadOnlyList mods) + => base.UpdateOverlayInformation(roomMods.Concat(mods).ToList()); } } From fdc0636554ca83daf3762c229feee047f34966b5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 17:30:31 +0300 Subject: [PATCH 41/88] General code cleanup --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 +++++----- .../OnlinePlay/Match/RoomModSelectOverlay.cs | 22 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index fa87b0aa0d..bf43dc3d9c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -274,12 +274,12 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight }, - beatmapAttributesDisplay = CreateBeatmapAttributesDisplay().With(b => + beatmapAttributesDisplay = new BeatmapAttributesDisplay { - b.Anchor = Anchor.BottomRight; - b.Origin = Anchor.BottomRight; - b.BeatmapInfo.Value = Beatmap?.BeatmapInfo; - }), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BeatmapInfo = { Value = Beatmap?.BeatmapInfo }, + }, } }); } @@ -289,8 +289,6 @@ namespace osu.Game.Overlays.Mods textSearchStartsActive = configManager.GetBindable(OsuSetting.ModSelectTextSearchStartsActive); } - protected virtual BeatmapAttributesDisplay CreateBeatmapAttributesDisplay() => new BeatmapAttributesDisplay(); - public override void Hide() { base.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index dd0ecbad90..db7cfe980c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -16,32 +16,32 @@ namespace osu.Game.Screens.OnlinePlay.Match { public partial class RoomModSelectOverlay : UserModSelectOverlay { - public RoomModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Plum) - : base(colourScheme) - { - } - - [Resolved(CanBeNull = true)] - private IBindable? selectedItem { get; set; } + [Resolved] + private IBindable selectedItem { get; set; } = null!; [Resolved] private RulesetStore rulesets { get; set; } = null!; private readonly List roomMods = new List(); + public RoomModSelectOverlay() + : base(OverlayColourScheme.Plum) + { + } + protected override void LoadComplete() { base.LoadComplete(); - selectedItem?.BindValueChanged(_ => + selectedItem.BindValueChanged(_ => { roomMods.Clear(); - if (selectedItem?.Value != null) + if (selectedItem.Value is PlaylistItem item) { - var rulesetInstance = rulesets.GetRuleset(selectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - roomMods.AddRange(selectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))); + roomMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); } SelectedMods.TriggerChange(); From c1db9d7819496e52db88223134d642d6cba6f0d3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 18:16:35 +0300 Subject: [PATCH 42/88] Add test coverage --- .../TestSceneMultiplayerMatchSubScreen.cs | 30 +++++++++++++++++++ .../Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a41eff067b..4bedc31f38 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -286,6 +286,36 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + [FlakyTest] // See above + public void TestModSelectOverlay() + { + AddStep("add playlist item", () => + { + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] + { + new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 2.0 } }), + new APIMod(new OsuModStrictTracking()), + }, + AllowedMods = new[] + { + new APIMod(new OsuModFlashlight()), + new APIMod(new OsuModHardRock()), + } + }); + }); + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + ClickButtonWhenEnabled(); + AddAssert("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); + AddAssert("mod select shows different multiplier", () => !this.ChildrenOfType().Single().ModMultiplier.IsDefault); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 8f84b51127..472fe6f476 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Mods RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint); } - private partial class BPMDisplay : RollingCounter + public partial class BPMDisplay : RollingCounter { protected override double RollingDuration => 250; From 771cdf9cd6eb12de88691ec9cddfecd52e586c88 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 18:30:14 +0300 Subject: [PATCH 43/88] Fix `TestSceneModEffectPreviewPanel` --- .../Visual/UserInterface/TestSceneModEffectPreviewPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs index b3ad5a499e..cdb6900f06 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs @@ -57,6 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Mods = { BindTarget = SelectedMods }, }); AddStep("set beatmap", () => From d4bc3090e7fd680f0f3452a1e242fa29b09f0178 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 23 Feb 2024 18:42:07 +0300 Subject: [PATCH 44/88] Fix incorrect conflict resolution --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 3432529976..7035af1594 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays.Mods RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint); } - public partial class BPMDisplay : RollingCounter + public partial class BPMDisplay : RollingCounter { protected override double RollingDuration => 250; From 2b73d816a70fab655cdf7a9f716d6630b333ed54 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 26 Feb 2024 21:26:35 +0300 Subject: [PATCH 45/88] Bring back mod setting tracker in `BeatmapAttributesDisplay` --- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 12 +++++++++++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 7 +------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 7035af1594..c58cf710bd 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -13,6 +13,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -43,6 +44,8 @@ namespace osu.Game.Overlays.Mods public BindableBool Collapsed { get; } = new BindableBool(true); + private ModSettingChangeTracker? modSettingChangeTracker; + [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; @@ -97,7 +100,14 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - Mods.BindValueChanged(_ => updateValues()); + Mods.BindValueChanged(_ => + { + modSettingChangeTracker?.Dispose(); + modSettingChangeTracker = new ModSettingChangeTracker(Mods.Value); + modSettingChangeTracker.SettingChanged += _ => updateValues(); + updateValues(); + }, true); + BeatmapInfo.BindValueChanged(_ => updateValues()); Collapsed.BindValueChanged(_ => diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index bf43dc3d9c..b7e2f744fa 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -468,12 +468,7 @@ namespace osu.Game.Overlays.Mods } if (beatmapAttributesDisplay != null) - { - if (!ReferenceEquals(beatmapAttributesDisplay.Mods.Value, mods)) - beatmapAttributesDisplay.Mods.Value = mods; - else - beatmapAttributesDisplay.Mods.TriggerChange(); // mods list may be same but a mod setting has changed, trigger change in that case. - } + beatmapAttributesDisplay.Mods.Value = mods; } private void updateCustomisation() From 3f9fbb931849a5f6276de638e621a1e341149cc3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 26 Feb 2024 22:25:04 +0300 Subject: [PATCH 46/88] Introduce the concept of `ActiveMods` in mod select overlay and rewrite once more --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 37 +++++++++++++------ .../OnlinePlay/Match/RoomModSelectOverlay.cs | 21 +++-------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b7e2f744fa..6b80ff6e44 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -42,6 +42,14 @@ namespace osu.Game.Overlays.Mods [Cached] public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); + /// + /// Contains a list of mods which should read from to display effects on the selected beatmap. + /// + /// + /// This is different from in screens like online-play rooms, where there are required mods activated from the playlist. + /// + public Bindable> ActiveMods { get; private set; } = new Bindable>(Array.Empty()); + /// /// Contains a dictionary with the current of all mods applicable for the current ruleset. /// @@ -313,22 +321,29 @@ namespace osu.Game.Overlays.Mods if (AllowCustomisation) ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); - SelectedMods.BindValueChanged(_ => + SelectedMods.BindValueChanged(mods => { - UpdateOverlayInformation(SelectedMods.Value); + var newMods = ActiveMods.Value.Except(mods.OldValue).Concat(mods.NewValue).ToList(); + ActiveMods.Value = newMods; + updateFromExternalSelection(); updateCustomisation(); + }, true); + + ActiveMods.BindValueChanged(_ => + { + updateOverlayInformation(); modSettingChangeTracker?.Dispose(); if (AllowCustomisation) { - // Importantly, use SelectedMods.Value here (and not the ValueChanged NewValue) as the latter can + // Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can // potentially be stale, due to complexities in the way change trackers work. // // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 - modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value); - modSettingChangeTracker.SettingChanged += _ => UpdateOverlayInformation(SelectedMods.Value); + modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value); + modSettingChangeTracker.SettingChanged += _ => updateOverlayInformation(); } }, true); @@ -451,24 +466,24 @@ namespace osu.Game.Overlays.Mods } /// - /// Updates any information displayed on the overlay regarding the effects of the selected mods. + /// Updates any information displayed on the overlay regarding the effects of the active mods. + /// This reads from instead of . /// - /// The list of mods to show effect from. This can be overriden to include effect of mods that are not part of the bindable (e.g. room mods in multiplayer/playlists). - protected virtual void UpdateOverlayInformation(IReadOnlyList mods) + private void updateOverlayInformation() { if (rankingInformationDisplay != null) { double multiplier = 1.0; - foreach (var mod in mods) + foreach (var mod in ActiveMods.Value) multiplier *= mod.ScoreMultiplier; rankingInformationDisplay.ModMultiplier.Value = multiplier; - rankingInformationDisplay.Ranked.Value = mods.All(m => m.Ranked); + rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked); } if (beatmapAttributesDisplay != null) - beatmapAttributesDisplay.Mods.Value = mods; + beatmapAttributesDisplay.Mods.Value = ActiveMods.Value; } private void updateCustomisation() diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index db7cfe980c..d3fd6de911 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -1,7 +1,6 @@ // 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 System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -10,7 +9,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.OnlinePlay.Match { @@ -22,8 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } = null!; - private readonly List roomMods = new List(); - public RoomModSelectOverlay() : base(OverlayColourScheme.Plum) { @@ -33,22 +29,17 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - selectedItem.BindValueChanged(_ => + selectedItem.BindValueChanged(v => { - roomMods.Clear(); - - if (selectedItem.Value is PlaylistItem item) + if (v.NewValue is PlaylistItem item) { var rulesetInstance = rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - roomMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); + ActiveMods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedMods.Value).ToList(); } - - SelectedMods.TriggerChange(); - }); + else + ActiveMods.Value = SelectedMods.Value; + }, true); } - - protected override void UpdateOverlayInformation(IReadOnlyList mods) - => base.UpdateOverlayInformation(roomMods.Concat(mods).ToList()); } } From 2151e0a2947e4c2cc4b5b071c2af1fce852359b5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 26 Feb 2024 22:25:20 +0300 Subject: [PATCH 47/88] Improve test coverage --- .../TestSceneMultiplayerMatchSubScreen.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 4bedc31f38..bdfe01ba09 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; @@ -303,7 +304,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AllowedMods = new[] { new APIMod(new OsuModFlashlight()), - new APIMod(new OsuModHardRock()), } }); }); @@ -312,8 +312,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); ClickButtonWhenEnabled(); - AddAssert("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); - AddAssert("mod select shows different multiplier", () => !this.ChildrenOfType().Single().ModMultiplier.IsDefault); + AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().Ranked.Value == false); + AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); + + AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType().Single(m => m.Mod is ModFlashlight).TriggerClick()); + AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); + + AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200); + AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen From 4a4ef91bc96c9cab11102156c429a778237cdcbf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 29 Feb 2024 00:42:52 +0300 Subject: [PATCH 48/88] Simplify active mods computation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 9 +++++---- .../OnlinePlay/Match/RoomModSelectOverlay.cs | 14 +++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 6b80ff6e44..9605c0b2fe 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -104,6 +104,8 @@ namespace osu.Game.Overlays.Mods protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; + protected virtual IReadOnlyList ComputeActiveMods() => SelectedMods.Value; + protected virtual IEnumerable CreateFooterButtons() { if (AllowCustomisation) @@ -321,13 +323,12 @@ namespace osu.Game.Overlays.Mods if (AllowCustomisation) ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); - SelectedMods.BindValueChanged(mods => + SelectedMods.BindValueChanged(_ => { - var newMods = ActiveMods.Value.Except(mods.OldValue).Concat(mods.NewValue).ToList(); - ActiveMods.Value = newMods; - updateFromExternalSelection(); updateCustomisation(); + + ActiveMods.Value = ComputeActiveMods(); }, true); ActiveMods.BindValueChanged(_ => diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index d3fd6de911..55e077df0f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -1,6 +1,7 @@ // 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 System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -9,6 +10,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.OnlinePlay.Match { @@ -20,6 +22,8 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved] private RulesetStore rulesets { get; set; } = null!; + private readonly List roomRequiredMods = new List(); + public RoomModSelectOverlay() : base(OverlayColourScheme.Plum) { @@ -31,15 +35,19 @@ namespace osu.Game.Screens.OnlinePlay.Match selectedItem.BindValueChanged(v => { + roomRequiredMods.Clear(); + if (v.NewValue is PlaylistItem item) { var rulesetInstance = rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - ActiveMods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedMods.Value).ToList(); + roomRequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); } - else - ActiveMods.Value = SelectedMods.Value; + + ActiveMods.Value = ComputeActiveMods(); }, true); } + + protected override IReadOnlyList ComputeActiveMods() => roomRequiredMods.Concat(base.ComputeActiveMods()).ToList(); } } From 7b28a66fc074a869d3af3f86ca2cf5c1d5e463d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 29 Feb 2024 11:11:30 +0100 Subject: [PATCH 49/88] Add failing test case --- .../TestSceneSliderInput.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 12be74c4cc..286e4bd775 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -457,6 +457,33 @@ namespace osu.Game.Rulesets.Osu.Tests assertMidSliderJudgementFail(); } + [Test] + public void TestRewindHandling() + { + performTest(new List + { + new OsuReplayFrame { Position = new Vector2(0), Actions = { OsuAction.LeftButton }, Time = time_slider_start }, + new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = 3250 }, + new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, + }, new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + Path = new SliderPath(PathType.PERFECT_CURVE, new[] + { + Vector2.Zero, + new Vector2(250, 0), + }, 250), + }); + + AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit())); + + AddStep("rewind to middle of slider", () => currentPlayer.Seek(time_during_slide_4)); + AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit())); + } + private void assertAllMaxJudgements() { AddAssert("All judgements max", () => From d05b31933fabb30da23e90cad95f3ea91ac40e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 29 Feb 2024 11:44:14 +0100 Subject: [PATCH 50/88] Fix slider tracking state not restoring correctly in all cases on rewind --- .../Objects/Drawables/SliderInputManager.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs index 148cf79337..58fa04bb4c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -21,6 +23,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public bool Tracking { get; private set; } + [Resolved] + private IGameplayClock? gameplayClock { get; set; } + + private readonly Stack<(double time, bool tracking)> trackingHistory = new Stack<(double, bool)>(); + /// /// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle. /// @@ -208,6 +215,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Whether the current mouse position is valid to begin tracking. private void updateTracking(bool isValidTrackingPosition) { + if (gameplayClock?.IsRewinding == true) + { + while (trackingHistory.TryPeek(out var historyEntry) && Time.Current < historyEntry.time) + trackingHistory.Pop(); + + Debug.Assert(trackingHistory.Count > 0); + + Tracking = trackingHistory.Peek().tracking; + return; + } + + bool wasTracking = Tracking; + // from the point at which the head circle is hit, this will be non-null. // it may be null if the head circle was missed. OsuAction? headCircleHitAction = getInitialHitAction(); @@ -247,6 +267,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables && isValidTrackingPosition // valid action && validTrackingAction; + + if (wasTracking != Tracking) + trackingHistory.Push((Time.Current, Tracking)); } private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction; From 1d1db951f08731604676bcca3c470a8416e23dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 29 Feb 2024 11:54:42 +0100 Subject: [PATCH 51/88] Reset slider input manager state completely on new object application Kind of scary this wasn't happening already. Mirrors `SpinnerRotationTracker`. --- .../Objects/Drawables/SliderInputManager.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs index 58fa04bb4c..5daf8ed972 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Play; using osuTK; @@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public SliderInputManager(DrawableSlider slider) { this.slider = slider; + this.slider.HitObjectApplied += resetState; } /// @@ -287,5 +289,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return action == OsuAction.LeftButton || action == OsuAction.RightButton; } + + private void resetState(DrawableHitObject obj) + { + Tracking = false; + trackingHistory.Clear(); + trackingHistory.Push((double.NegativeInfinity, false)); + timeToAcceptAnyKeyAfter = null; + lastPressedActions.Clear(); + screenSpaceMousePosition = null; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + slider.HitObjectApplied -= resetState; + } } } From 876b80642357996f90e8469cc25eb1e490f1f224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 29 Feb 2024 12:11:50 +0100 Subject: [PATCH 52/88] Store tracking history to slider judgement result instead --- .../Judgements/OsuSliderJudgementResult.cs | 20 +++++++++++++++++++ .../Objects/Drawables/DrawableSlider.cs | 6 ++++++ .../Objects/Drawables/SliderInputManager.cs | 7 ++----- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuSliderJudgementResult.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSliderJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSliderJudgementResult.cs new file mode 100644 index 0000000000..f52294cdd7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSliderJudgementResult.cs @@ -0,0 +1,20 @@ +// 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.Objects; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuSliderJudgementResult : OsuJudgementResult + { + public readonly Stack<(double time, bool tracking)> TrackingHistory = new Stack<(double, bool)>(); + + public OsuSliderJudgementResult(HitObject hitObject, Judgement judgement) + : base(hitObject, judgement) + { + TrackingHistory.Push((double.NegativeInfinity, false)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index b7ce712e2c..e519e51562 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -14,8 +14,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Audio; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new Slider HitObject => (Slider)base.HitObject; + public new OsuSliderJudgementResult Result => (OsuSliderJudgementResult)base.Result; + public DrawableSliderHead HeadCircle => headContainer.Child; public DrawableSliderTail TailCircle => tailContainer.Child; @@ -134,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, true); } + protected override JudgementResult CreateResult(Judgement judgement) => new OsuSliderJudgementResult(HitObject, judgement); + protected override void OnApply() { base.OnApply(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs index 5daf8ed972..c75ae35a1a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [Resolved] private IGameplayClock? gameplayClock { get; set; } - private readonly Stack<(double time, bool tracking)> trackingHistory = new Stack<(double, bool)>(); - /// /// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle. /// @@ -219,6 +217,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (gameplayClock?.IsRewinding == true) { + var trackingHistory = slider.Result.TrackingHistory; while (trackingHistory.TryPeek(out var historyEntry) && Time.Current < historyEntry.time) trackingHistory.Pop(); @@ -271,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables && validTrackingAction; if (wasTracking != Tracking) - trackingHistory.Push((Time.Current, Tracking)); + slider.Result.TrackingHistory.Push((Time.Current, Tracking)); } private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction; @@ -293,8 +292,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void resetState(DrawableHitObject obj) { Tracking = false; - trackingHistory.Clear(); - trackingHistory.Push((double.NegativeInfinity, false)); timeToAcceptAnyKeyAfter = null; lastPressedActions.Clear(); screenSpaceMousePosition = null; From 77b3055978bdd3b872d4297f1b70ba05f6164c05 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 29 Feb 2024 23:23:57 +0300 Subject: [PATCH 53/88] Improve sb sprite end time guessing --- osu.Game/Storyboards/CommandTimelineGroup.cs | 41 ++-------------- osu.Game/Storyboards/StoryboardSprite.cs | 50 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 0b96db6861..c899cf77d3 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics; @@ -46,32 +45,10 @@ namespace osu.Game.Storyboards } [JsonIgnore] - public double CommandsStartTime - { - get - { - double min = double.MaxValue; - - for (int i = 0; i < timelines.Length; i++) - min = Math.Min(min, timelines[i].StartTime); - - return min; - } - } + public double CommandsStartTime => timelines.Min(static t => t.StartTime); [JsonIgnore] - public double CommandsEndTime - { - get - { - double max = double.MinValue; - - for (int i = 0; i < timelines.Length; i++) - max = Math.Max(max, timelines[i].EndTime); - - return max; - } - } + public double CommandsEndTime => timelines.Max(static t => t.EndTime); [JsonIgnore] public double CommandsDuration => CommandsEndTime - CommandsStartTime; @@ -83,19 +60,7 @@ namespace osu.Game.Storyboards public virtual double EndTime => CommandsEndTime; [JsonIgnore] - public bool HasCommands - { - get - { - for (int i = 0; i < timelines.Length; i++) - { - if (timelines[i].HasCommands) - return true; - } - - return false; - } - } + public bool HasCommands => timelines.Any(static t => t.HasCommands); public virtual IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 982185d51b..350438942e 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -85,12 +85,56 @@ namespace osu.Game.Storyboards { get { - double latestEndTime = TimelineGroup.EndTime; + double latestEndTime = double.MaxValue; + + // Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex. + if (loops.Count == 0) + { + // Here we are starting from maximum value and trying to minimise the end time on each step. + // There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0. + double[] deathTimes = + { + double.MaxValue, // alpha + double.MaxValue, // colour alpha + double.MaxValue, // scale + double.MaxValue, // scale x + double.MaxValue, // scale y + }; + + // The loops below are following the same pattern. + // We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row + // so we are saving the earliest of them. + foreach (var alphaCommand in TimelineGroup.Alpha.Commands) + { + deathTimes[0] = alphaCommand.EndValue == 0 + ? Math.Min(alphaCommand.EndTime, deathTimes[0]) // commands are ordered by the start time, however end time may vary. Save the earliest. + : double.MaxValue; // If value isn't 0 (sprite becomes visible again), revert the saved state. + } + + foreach (var colourCommand in TimelineGroup.Colour.Commands) + deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue; + + foreach (var scaleCommand in TimelineGroup.Scale.Commands) + deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue; + + foreach (var scaleCommand in TimelineGroup.VectorScale.Commands) + { + deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue; + deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue; + } + + // Take the minimum time of all the potential "death" reasons. + latestEndTime = deathTimes.Min(); + } + + // If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue + // and thus conservativeEndTime will be used. + double conservativeEndTime = TimelineGroup.EndTime; foreach (var l in loops) - latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); + conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); - return latestEndTime; + return Math.Min(latestEndTime, conservativeEndTime); } } From e0c73eb36225b1e7e058b3af5cc553f791ae28b6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Mar 2024 22:49:12 +0300 Subject: [PATCH 54/88] Fix osu!mania key images potentially showing gaps between columns --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs index 48b92a8486..9fd33b9963 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Rulesets.Mania.UI; @@ -49,14 +50,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy upSprite = new Sprite { Origin = Anchor.BottomCentre, - Texture = skin.GetTexture(upImage), + Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge), RelativeSizeAxes = Axes.X, Width = 1 }, downSprite = new Sprite { Origin = Anchor.BottomCentre, - Texture = skin.GetTexture(downImage), + Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge), RelativeSizeAxes = Axes.X, Width = 1, Alpha = 0 From f1b66da469ead2105e6b3f1d83e95e04b7f4ec45 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Mar 2024 22:57:13 +0300 Subject: [PATCH 55/88] Add comments --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs index 9fd33b9963..6ba91fbbd5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs @@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy upSprite = new Sprite { Origin = Anchor.BottomCentre, + // ClampToEdge is used to avoid gaps between keys, see: https://github.com/ppy/osu/issues/27431 Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge), RelativeSizeAxes = Axes.X, Width = 1 @@ -57,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy downSprite = new Sprite { Origin = Anchor.BottomCentre, + // ClampToEdge is used to avoid gaps between keys, see: https://github.com/ppy/osu/issues/27431 Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge), RelativeSizeAxes = Axes.X, Width = 1, From 82373ff752ab9c1d961a8c5673499ddb913bec05 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Mar 2024 03:18:42 +0300 Subject: [PATCH 56/88] Add failing catch beatmap --- .../CatchBeatmapConversionTest.cs | 1 + .../Beatmaps/1041052-expected-conversion.json | 1 + .../Resources/Testing/Beatmaps/1041052.osu | 210 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index d0ecb828df..81e0675aaa 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("112643")] + [TestCase("1041052", new[] { typeof(CatchModHardRock) })] public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json new file mode 100644 index 0000000000..01150e701d --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":1155.0,"Objects":[{"StartTime":1155.0,"Position":208.0,"HyperDash":false},{"StartTime":1251.0,"Position":167.675858,"HyperDash":false},{"StartTime":1348.0,"Position":157.087921,"HyperDash":false},{"StartTime":1445.0,"Position":128.5,"HyperDash":false},{"StartTime":1542.0,"Position":105.912064,"HyperDash":false},{"StartTime":1620.0,"Position":102.336205,"HyperDash":false},{"StartTime":1735.0,"Position":55.0,"HyperDash":false}]},{"StartTime":2122.0,"Objects":[{"StartTime":2122.0,"Position":275.0,"HyperDash":false}]},{"StartTime":2509.0,"Objects":[{"StartTime":2509.0,"Position":269.0,"HyperDash":false}]},{"StartTime":3284.0,"Objects":[{"StartTime":3284.0,"Position":448.0,"HyperDash":false},{"StartTime":3380.0,"Position":466.562317,"HyperDash":false},{"StartTime":3477.0,"Position":477.575867,"HyperDash":false},{"StartTime":3556.0,"Position":467.440033,"HyperDash":false},{"StartTime":3671.0,"Position":481.146881,"HyperDash":false}]},{"StartTime":4058.0,"Objects":[{"StartTime":4058.0,"Position":288.0,"HyperDash":false}]},{"StartTime":5025.0,"Objects":[{"StartTime":5025.0,"Position":128.0,"HyperDash":false},{"StartTime":5121.0,"Position":94.67586,"HyperDash":false},{"StartTime":5218.0,"Position":77.08793,"HyperDash":false},{"StartTime":5315.0,"Position":51.5,"HyperDash":false},{"StartTime":5412.0,"Position":77.08794,"HyperDash":false},{"StartTime":5490.0,"Position":111.663795,"HyperDash":false},{"StartTime":5605.0,"Position":128.0,"HyperDash":false}]},{"StartTime":5800.0,"Objects":[{"StartTime":5800.0,"Position":352.0,"HyperDash":false}]},{"StartTime":5993.0,"Objects":[{"StartTime":5993.0,"Position":240.0,"HyperDash":false}]},{"StartTime":6187.0,"Objects":[{"StartTime":6187.0,"Position":336.0,"HyperDash":false}]},{"StartTime":6574.0,"Objects":[{"StartTime":6574.0,"Position":416.0,"HyperDash":false},{"StartTime":6670.0,"Position":394.697662,"HyperDash":false},{"StartTime":6767.0,"Position":365.131775,"HyperDash":false},{"StartTime":6864.0,"Position":344.5659,"HyperDash":false},{"StartTime":6961.0,"Position":314.0,"HyperDash":false},{"StartTime":7057.0,"Position":279.6977,"HyperDash":false},{"StartTime":7154.0,"Position":263.131775,"HyperDash":false},{"StartTime":7233.0,"Position":259.310059,"HyperDash":false},{"StartTime":7348.0,"Position":212.0,"HyperDash":false}]},{"StartTime":8122.0,"Objects":[{"StartTime":8122.0,"Position":488.0,"HyperDash":false},{"StartTime":8218.0,"Position":475.697662,"HyperDash":false},{"StartTime":8315.0,"Position":437.131775,"HyperDash":false},{"StartTime":8394.0,"Position":399.3101,"HyperDash":false},{"StartTime":8509.0,"Position":386.0,"HyperDash":false}]},{"StartTime":8896.0,"Objects":[{"StartTime":8896.0,"Position":457.0,"HyperDash":false},{"StartTime":8992.0,"Position":444.675873,"HyperDash":false},{"StartTime":9089.0,"Position":406.087921,"HyperDash":false},{"StartTime":9186.0,"Position":384.5,"HyperDash":false},{"StartTime":9283.0,"Position":354.912048,"HyperDash":false},{"StartTime":9361.0,"Position":330.3362,"HyperDash":false},{"StartTime":9476.0,"Position":304.0,"HyperDash":false}]},{"StartTime":10058.0,"Objects":[{"StartTime":10058.0,"Position":400.0,"HyperDash":false}]},{"StartTime":10445.0,"Objects":[{"StartTime":10445.0,"Position":304.0,"HyperDash":false},{"StartTime":10541.0,"Position":277.697662,"HyperDash":false},{"StartTime":10638.0,"Position":253.131775,"HyperDash":false},{"StartTime":10735.0,"Position":212.565887,"HyperDash":false},{"StartTime":10832.0,"Position":202.0,"HyperDash":false},{"StartTime":10928.0,"Position":208.302322,"HyperDash":false},{"StartTime":11025.0,"Position":252.868225,"HyperDash":false},{"StartTime":11104.0,"Position":286.6899,"HyperDash":false},{"StartTime":11219.0,"Position":304.0,"HyperDash":false}]},{"StartTime":11606.0,"Objects":[{"StartTime":11606.0,"Position":400.0,"HyperDash":false}]},{"StartTime":11993.0,"Objects":[{"StartTime":11993.0,"Position":240.0,"HyperDash":false},{"StartTime":12089.0,"Position":231.675858,"HyperDash":false},{"StartTime":12186.0,"Position":189.087921,"HyperDash":false},{"StartTime":12283.0,"Position":156.5,"HyperDash":false},{"StartTime":12380.0,"Position":137.912064,"HyperDash":false},{"StartTime":12458.0,"Position":136.336212,"HyperDash":false},{"StartTime":12573.0,"Position":87.0,"HyperDash":false}]},{"StartTime":13154.0,"Objects":[{"StartTime":13154.0,"Position":0.0,"HyperDash":false}]},{"StartTime":13542.0,"Objects":[{"StartTime":13542.0,"Position":112.0,"HyperDash":false},{"StartTime":13638.0,"Position":119.913734,"HyperDash":false},{"StartTime":13735.0,"Position":144.5726,"HyperDash":false},{"StartTime":13832.0,"Position":179.8026,"HyperDash":false},{"StartTime":13929.0,"Position":191.357681,"HyperDash":false},{"StartTime":14007.0,"Position":228.85704,"HyperDash":false},{"StartTime":14122.0,"Position":241.622177,"HyperDash":false}]},{"StartTime":14316.0,"Objects":[{"StartTime":14316.0,"Position":288.0,"HyperDash":false},{"StartTime":14412.0,"Position":328.324127,"HyperDash":false},{"StartTime":14509.0,"Position":338.912079,"HyperDash":false},{"StartTime":14606.0,"Position":364.5,"HyperDash":false},{"StartTime":14703.0,"Position":338.912079,"HyperDash":false},{"StartTime":14781.0,"Position":301.3362,"HyperDash":false},{"StartTime":14896.0,"Position":288.0,"HyperDash":false}]},{"StartTime":15284.0,"Objects":[{"StartTime":15284.0,"Position":192.0,"HyperDash":false},{"StartTime":15362.0,"Position":187.7823,"HyperDash":false},{"StartTime":15477.0,"Position":169.192108,"HyperDash":false}]},{"StartTime":15864.0,"Objects":[{"StartTime":15864.0,"Position":464.0,"HyperDash":false}]},{"StartTime":16638.0,"Objects":[{"StartTime":16638.0,"Position":128.0,"HyperDash":false},{"StartTime":16734.0,"Position":90.86488,"HyperDash":false},{"StartTime":16831.0,"Position":78.3134155,"HyperDash":false},{"StartTime":16928.0,"Position":73.3517761,"HyperDash":false},{"StartTime":17025.0,"Position":34.6746674,"HyperDash":false},{"StartTime":17121.0,"Position":33.0714569,"HyperDash":false},{"StartTime":17218.0,"Position":2.65127468,"HyperDash":false},{"StartTime":17315.0,"Position":19.907608,"HyperDash":false},{"StartTime":17412.0,"Position":34.674675,"HyperDash":false},{"StartTime":17508.0,"Position":56.12827,"HyperDash":false},{"StartTime":17605.0,"Position":78.0694351,"HyperDash":false},{"StartTime":17684.0,"Position":97.97488,"HyperDash":false},{"StartTime":17799.0,"Position":128.0,"HyperDash":false}]},{"StartTime":18187.0,"Objects":[{"StartTime":18187.0,"Position":224.0,"HyperDash":false},{"StartTime":18283.0,"Position":231.165024,"HyperDash":false},{"StartTime":18380.0,"Position":273.681732,"HyperDash":false},{"StartTime":18477.0,"Position":304.374176,"HyperDash":false},{"StartTime":18574.0,"Position":316.397827,"HyperDash":false},{"StartTime":18670.0,"Position":312.8548,"HyperDash":false},{"StartTime":18767.0,"Position":345.5291,"HyperDash":false},{"StartTime":18864.0,"Position":328.007355,"HyperDash":false},{"StartTime":18961.0,"Position":316.397827,"HyperDash":false},{"StartTime":19057.0,"Position":304.594452,"HyperDash":false},{"StartTime":19154.0,"Position":273.924957,"HyperDash":false},{"StartTime":19233.0,"Position":271.063354,"HyperDash":false},{"StartTime":19348.0,"Position":224.0,"HyperDash":false}]},{"StartTime":19735.0,"Objects":[{"StartTime":19735.0,"Position":128.0,"HyperDash":false},{"StartTime":19831.0,"Position":136.324142,"HyperDash":false},{"StartTime":19928.0,"Position":178.912079,"HyperDash":false},{"StartTime":20025.0,"Position":223.5,"HyperDash":false},{"StartTime":20122.0,"Position":230.087936,"HyperDash":false},{"StartTime":20200.0,"Position":260.6638,"HyperDash":false},{"StartTime":20315.0,"Position":281.0,"HyperDash":false}]},{"StartTime":20896.0,"Objects":[{"StartTime":20896.0,"Position":432.0,"HyperDash":false}]},{"StartTime":21284.0,"Objects":[{"StartTime":21284.0,"Position":328.0,"HyperDash":false},{"StartTime":21380.0,"Position":357.324127,"HyperDash":false},{"StartTime":21477.0,"Position":378.912079,"HyperDash":false},{"StartTime":21574.0,"Position":420.5,"HyperDash":false},{"StartTime":21671.0,"Position":430.087952,"HyperDash":false},{"StartTime":21749.0,"Position":447.6638,"HyperDash":false},{"StartTime":21864.0,"Position":481.0,"HyperDash":false}]},{"StartTime":22445.0,"Objects":[{"StartTime":22445.0,"Position":328.0,"HyperDash":false}]},{"StartTime":22832.0,"Objects":[{"StartTime":22832.0,"Position":224.0,"HyperDash":false},{"StartTime":22928.0,"Position":188.675858,"HyperDash":false},{"StartTime":23025.0,"Position":173.087921,"HyperDash":false},{"StartTime":23122.0,"Position":156.5,"HyperDash":false},{"StartTime":23219.0,"Position":121.912064,"HyperDash":false},{"StartTime":23297.0,"Position":91.3362045,"HyperDash":false},{"StartTime":23412.0,"Position":71.0,"HyperDash":false}]},{"StartTime":23993.0,"Objects":[{"StartTime":23993.0,"Position":224.0,"HyperDash":false}]},{"StartTime":24380.0,"Objects":[{"StartTime":24380.0,"Position":112.0,"HyperDash":false},{"StartTime":24476.0,"Position":127.324142,"HyperDash":false},{"StartTime":24573.0,"Position":162.912079,"HyperDash":false},{"StartTime":24670.0,"Position":202.5,"HyperDash":false},{"StartTime":24767.0,"Position":214.087936,"HyperDash":false},{"StartTime":24845.0,"Position":246.663788,"HyperDash":false},{"StartTime":24960.0,"Position":265.0,"HyperDash":false}]},{"StartTime":25541.0,"Objects":[{"StartTime":25541.0,"Position":416.0,"HyperDash":false}]},{"StartTime":25929.0,"Objects":[{"StartTime":25929.0,"Position":304.0,"HyperDash":false},{"StartTime":26025.0,"Position":287.9714,"HyperDash":false},{"StartTime":26122.0,"Position":274.232758,"HyperDash":false},{"StartTime":26219.0,"Position":253.164063,"HyperDash":false},{"StartTime":26316.0,"Position":274.1704,"HyperDash":false},{"StartTime":26394.0,"Position":290.949921,"HyperDash":false},{"StartTime":26509.0,"Position":303.819763,"HyperDash":false}]},{"StartTime":27090.0,"Objects":[{"StartTime":27090.0,"Position":480.0,"HyperDash":false}]},{"StartTime":27477.0,"Objects":[{"StartTime":27477.0,"Position":384.0,"HyperDash":false},{"StartTime":27573.0,"Position":351.697662,"HyperDash":false},{"StartTime":27670.0,"Position":333.0,"HyperDash":false},{"StartTime":27749.0,"Position":356.6899,"HyperDash":false},{"StartTime":27864.0,"Position":384.0,"HyperDash":false}]},{"StartTime":28058.0,"Objects":[{"StartTime":28058.0,"Position":432.0,"HyperDash":false}]},{"StartTime":28445.0,"Objects":[{"StartTime":28445.0,"Position":333.0,"HyperDash":false},{"StartTime":28541.0,"Position":305.697662,"HyperDash":false},{"StartTime":28638.0,"Position":282.0,"HyperDash":false},{"StartTime":28717.0,"Position":319.6899,"HyperDash":false},{"StartTime":28832.0,"Position":333.0,"HyperDash":false}]},{"StartTime":29025.0,"Objects":[{"StartTime":29025.0,"Position":384.0,"HyperDash":false},{"StartTime":29121.0,"Position":341.697662,"HyperDash":false},{"StartTime":29218.0,"Position":333.131775,"HyperDash":false},{"StartTime":29297.0,"Position":293.3101,"HyperDash":false},{"StartTime":29412.0,"Position":282.0,"HyperDash":false}]},{"StartTime":29606.0,"Objects":[{"StartTime":29606.0,"Position":224.0,"HyperDash":false},{"StartTime":29702.0,"Position":206.103424,"HyperDash":false},{"StartTime":29799.0,"Position":176.49205,"HyperDash":false},{"StartTime":29896.0,"Position":149.701324,"HyperDash":false},{"StartTime":29993.0,"Position":147.519775,"HyperDash":false},{"StartTime":30071.0,"Position":144.128265,"HyperDash":false},{"StartTime":30186.0,"Position":148.650436,"HyperDash":false}]},{"StartTime":30574.0,"Objects":[{"StartTime":30574.0,"Position":272.0,"HyperDash":false},{"StartTime":30670.0,"Position":303.302338,"HyperDash":false},{"StartTime":30767.0,"Position":322.868225,"HyperDash":false},{"StartTime":30846.0,"Position":351.6899,"HyperDash":false},{"StartTime":30961.0,"Position":374.0,"HyperDash":false}]},{"StartTime":31154.0,"Objects":[{"StartTime":31154.0,"Position":424.0,"HyperDash":false},{"StartTime":31250.0,"Position":439.371674,"HyperDash":false},{"StartTime":31347.0,"Position":424.705872,"HyperDash":false},{"StartTime":31444.0,"Position":417.305573,"HyperDash":false},{"StartTime":31541.0,"Position":395.3253,"HyperDash":false},{"StartTime":31619.0,"Position":372.2993,"HyperDash":false},{"StartTime":31734.0,"Position":347.633759,"HyperDash":false}]},{"StartTime":32122.0,"Objects":[{"StartTime":32122.0,"Position":224.0,"HyperDash":false},{"StartTime":32218.0,"Position":200.129822,"HyperDash":false},{"StartTime":32315.0,"Position":176.418152,"HyperDash":false},{"StartTime":32394.0,"Position":164.275757,"HyperDash":false},{"StartTime":32509.0,"Position":146.329926,"HyperDash":false}]},{"StartTime":32703.0,"Objects":[{"StartTime":32703.0,"Position":256.0,"HyperDash":false}]},{"StartTime":33284.0,"Objects":[{"StartTime":33284.0,"Position":496.0,"HyperDash":false}]},{"StartTime":33671.0,"Objects":[{"StartTime":33671.0,"Position":304.0,"HyperDash":false},{"StartTime":33767.0,"Position":297.697662,"HyperDash":false},{"StartTime":33864.0,"Position":253.0,"HyperDash":false},{"StartTime":33943.0,"Position":270.6899,"HyperDash":false},{"StartTime":34058.0,"Position":304.0,"HyperDash":false}]},{"StartTime":34251.0,"Objects":[{"StartTime":34251.0,"Position":352.0,"HyperDash":false},{"StartTime":34347.0,"Position":374.083679,"HyperDash":false},{"StartTime":34444.0,"Position":391.360016,"HyperDash":false},{"StartTime":34541.0,"Position":387.812561,"HyperDash":false},{"StartTime":34638.0,"Position":404.369629,"HyperDash":false},{"StartTime":34716.0,"Position":417.578369,"HyperDash":false},{"StartTime":34831.0,"Position":385.75708,"HyperDash":false}]},{"StartTime":35219.0,"Objects":[{"StartTime":35219.0,"Position":280.0,"HyperDash":false},{"StartTime":35315.0,"Position":277.964783,"HyperDash":false},{"StartTime":35412.0,"Position":251.783386,"HyperDash":false},{"StartTime":35491.0,"Position":238.233582,"HyperDash":false},{"StartTime":35606.0,"Position":223.420578,"HyperDash":false}]},{"StartTime":35800.0,"Objects":[{"StartTime":35800.0,"Position":272.0,"HyperDash":false},{"StartTime":35896.0,"Position":303.035217,"HyperDash":false},{"StartTime":35993.0,"Position":300.2166,"HyperDash":false},{"StartTime":36072.0,"Position":326.766418,"HyperDash":false},{"StartTime":36187.0,"Position":328.5794,"HyperDash":false}]},{"StartTime":36380.0,"Objects":[{"StartTime":36380.0,"Position":224.0,"HyperDash":false}]},{"StartTime":36767.0,"Objects":[{"StartTime":36767.0,"Position":176.0,"HyperDash":false},{"StartTime":36863.0,"Position":148.9648,"HyperDash":false},{"StartTime":36960.0,"Position":147.783386,"HyperDash":false},{"StartTime":37039.0,"Position":140.233582,"HyperDash":false},{"StartTime":37154.0,"Position":119.420578,"HyperDash":false}]},{"StartTime":37348.0,"Objects":[{"StartTime":37348.0,"Position":168.0,"HyperDash":false},{"StartTime":37444.0,"Position":170.0352,"HyperDash":false},{"StartTime":37541.0,"Position":196.216614,"HyperDash":false},{"StartTime":37620.0,"Position":195.766418,"HyperDash":false},{"StartTime":37735.0,"Position":224.579422,"HyperDash":false}]},{"StartTime":37928.0,"Objects":[{"StartTime":37928.0,"Position":120.0,"HyperDash":false}]},{"StartTime":38316.0,"Objects":[{"StartTime":38316.0,"Position":304.0,"HyperDash":false},{"StartTime":38412.0,"Position":277.697662,"HyperDash":false},{"StartTime":38509.0,"Position":253.131775,"HyperDash":false},{"StartTime":38588.0,"Position":226.310089,"HyperDash":false},{"StartTime":38703.0,"Position":202.0,"HyperDash":false}]},{"StartTime":38896.0,"Objects":[{"StartTime":38896.0,"Position":88.0,"HyperDash":false}]},{"StartTime":39477.0,"Objects":[{"StartTime":39477.0,"Position":280.0,"HyperDash":false},{"StartTime":39555.0,"Position":292.6114,"HyperDash":false},{"StartTime":39670.0,"Position":331.0,"HyperDash":false}]},{"StartTime":39864.0,"Objects":[{"StartTime":39864.0,"Position":424.0,"HyperDash":false},{"StartTime":39960.0,"Position":428.059753,"HyperDash":false},{"StartTime":40057.0,"Position":441.062134,"HyperDash":false},{"StartTime":40136.0,"Position":432.009247,"HyperDash":false},{"StartTime":40251.0,"Position":420.508881,"HyperDash":false}]},{"StartTime":40445.0,"Objects":[{"StartTime":40445.0,"Position":288.0,"HyperDash":false}]},{"StartTime":41025.0,"Objects":[{"StartTime":41025.0,"Position":32.0,"HyperDash":false}]},{"StartTime":41413.0,"Objects":[{"StartTime":41413.0,"Position":256.0,"HyperDash":false},{"StartTime":41509.0,"Position":291.8391,"HyperDash":false},{"StartTime":41606.0,"Position":302.265045,"HyperDash":false},{"StartTime":41685.0,"Position":310.9909,"HyperDash":false},{"StartTime":41800.0,"Position":352.8495,"HyperDash":false}]},{"StartTime":41993.0,"Objects":[{"StartTime":41993.0,"Position":447.0,"HyperDash":false}]},{"StartTime":42187.0,"Objects":[{"StartTime":42187.0,"Position":440.0,"HyperDash":false},{"StartTime":42283.0,"Position":403.2554,"HyperDash":false},{"StartTime":42380.0,"Position":389.7651,"HyperDash":false},{"StartTime":42459.0,"Position":365.6506,"HyperDash":false},{"StartTime":42574.0,"Position":342.9871,"HyperDash":false}]},{"StartTime":42961.0,"Objects":[{"StartTime":42961.0,"Position":248.0,"HyperDash":false},{"StartTime":43057.0,"Position":264.94986,"HyperDash":false},{"StartTime":43154.0,"Position":281.6838,"HyperDash":false},{"StartTime":43251.0,"Position":305.2995,"HyperDash":false},{"StartTime":43348.0,"Position":330.6694,"HyperDash":false},{"StartTime":43444.0,"Position":352.264221,"HyperDash":false},{"StartTime":43541.0,"Position":377.479736,"HyperDash":false},{"StartTime":43638.0,"Position":371.509277,"HyperDash":false},{"StartTime":43735.0,"Position":330.6694,"HyperDash":false},{"StartTime":43831.0,"Position":294.557373,"HyperDash":false},{"StartTime":43928.0,"Position":281.915039,"HyperDash":false},{"StartTime":44007.0,"Position":283.462677,"HyperDash":false},{"StartTime":44122.0,"Position":248.0,"HyperDash":false}]},{"StartTime":44509.0,"Objects":[{"StartTime":44509.0,"Position":144.0,"HyperDash":false},{"StartTime":44605.0,"Position":140.034851,"HyperDash":false},{"StartTime":44702.0,"Position":110.27771,"HyperDash":false},{"StartTime":44799.0,"Position":69.63604,"HyperDash":false},{"StartTime":44896.0,"Position":61.2429733,"HyperDash":false},{"StartTime":44974.0,"Position":47.09105,"HyperDash":false},{"StartTime":45089.0,"Position":14.520277,"HyperDash":false}]},{"StartTime":45284.0,"Objects":[{"StartTime":45284.0,"Position":56.0,"HyperDash":false}]},{"StartTime":45671.0,"Objects":[{"StartTime":45671.0,"Position":264.0,"HyperDash":false}]},{"StartTime":46058.0,"Objects":[{"StartTime":46058.0,"Position":264.0,"HyperDash":false},{"StartTime":46154.0,"Position":301.302338,"HyperDash":false},{"StartTime":46251.0,"Position":314.868225,"HyperDash":false},{"StartTime":46330.0,"Position":321.6899,"HyperDash":false},{"StartTime":46445.0,"Position":366.0,"HyperDash":false}]},{"StartTime":46638.0,"Objects":[{"StartTime":46638.0,"Position":416.0,"HyperDash":false},{"StartTime":46734.0,"Position":371.675873,"HyperDash":false},{"StartTime":46831.0,"Position":365.087921,"HyperDash":false},{"StartTime":46928.0,"Position":325.5,"HyperDash":false},{"StartTime":47025.0,"Position":313.912048,"HyperDash":false},{"StartTime":47103.0,"Position":306.3362,"HyperDash":false},{"StartTime":47218.0,"Position":263.0,"HyperDash":false}]},{"StartTime":47606.0,"Objects":[{"StartTime":47606.0,"Position":360.0,"HyperDash":false},{"StartTime":47702.0,"Position":324.675873,"HyperDash":false},{"StartTime":47799.0,"Position":309.087921,"HyperDash":false},{"StartTime":47896.0,"Position":293.5,"HyperDash":false},{"StartTime":47993.0,"Position":257.912048,"HyperDash":false},{"StartTime":48071.0,"Position":244.336212,"HyperDash":false},{"StartTime":48186.0,"Position":207.0,"HyperDash":false}]},{"StartTime":48380.0,"Objects":[{"StartTime":48380.0,"Position":160.0,"HyperDash":false},{"StartTime":48476.0,"Position":166.997681,"HyperDash":false},{"StartTime":48573.0,"Position":187.484192,"HyperDash":false},{"StartTime":48652.0,"Position":200.988281,"HyperDash":false},{"StartTime":48767.0,"Position":236.72261,"HyperDash":false}]},{"StartTime":49154.0,"Objects":[{"StartTime":49154.0,"Position":32.0,"HyperDash":false}]},{"StartTime":49542.0,"Objects":[{"StartTime":49542.0,"Position":248.0,"HyperDash":false},{"StartTime":49638.0,"Position":266.302338,"HyperDash":false},{"StartTime":49735.0,"Position":298.868225,"HyperDash":false},{"StartTime":49814.0,"Position":314.6899,"HyperDash":false},{"StartTime":49929.0,"Position":350.0,"HyperDash":false}]},{"StartTime":50316.0,"Objects":[{"StartTime":50316.0,"Position":256.0,"HyperDash":false},{"StartTime":50394.0,"Position":235.3886,"HyperDash":false},{"StartTime":50509.0,"Position":205.0,"HyperDash":false}]},{"StartTime":50703.0,"Objects":[{"StartTime":50703.0,"Position":256.0,"HyperDash":false},{"StartTime":50799.0,"Position":296.302338,"HyperDash":false},{"StartTime":50896.0,"Position":306.868225,"HyperDash":false},{"StartTime":50975.0,"Position":344.6899,"HyperDash":false},{"StartTime":51090.0,"Position":358.0,"HyperDash":false}]},{"StartTime":51284.0,"Objects":[{"StartTime":51284.0,"Position":440.0,"HyperDash":false}]},{"StartTime":51477.0,"Objects":[{"StartTime":51477.0,"Position":352.0,"HyperDash":false},{"StartTime":51573.0,"Position":329.697662,"HyperDash":false},{"StartTime":51670.0,"Position":301.131775,"HyperDash":false},{"StartTime":51749.0,"Position":288.3101,"HyperDash":false},{"StartTime":51864.0,"Position":250.0,"HyperDash":false}]},{"StartTime":52251.0,"Objects":[{"StartTime":52251.0,"Position":128.0,"HyperDash":false},{"StartTime":52347.0,"Position":102.275604,"HyperDash":false},{"StartTime":52444.0,"Position":77.8078156,"HyperDash":false},{"StartTime":52541.0,"Position":41.6188354,"HyperDash":false},{"StartTime":52638.0,"Position":32.3890381,"HyperDash":false},{"StartTime":52716.0,"Position":11.4332657,"HyperDash":false},{"StartTime":52831.0,"Position":4.4833107,"HyperDash":false}]},{"StartTime":53025.0,"Objects":[{"StartTime":53025.0,"Position":88.0,"HyperDash":false}]},{"StartTime":53413.0,"Objects":[{"StartTime":53413.0,"Position":168.0,"HyperDash":false},{"StartTime":53491.0,"Position":145.000992,"HyperDash":false},{"StartTime":53606.0,"Position":155.630676,"HyperDash":false}]},{"StartTime":53800.0,"Objects":[{"StartTime":53800.0,"Position":248.0,"HyperDash":false},{"StartTime":53896.0,"Position":263.12973,"HyperDash":false},{"StartTime":53993.0,"Position":290.128967,"HyperDash":false},{"StartTime":54090.0,"Position":316.20874,"HyperDash":false},{"StartTime":54187.0,"Position":340.6195,"HyperDash":false},{"StartTime":54265.0,"Position":376.1075,"HyperDash":false},{"StartTime":54380.0,"Position":385.246277,"HyperDash":false}]},{"StartTime":54574.0,"Objects":[{"StartTime":54574.0,"Position":472.0,"HyperDash":false}]},{"StartTime":54961.0,"Objects":[{"StartTime":54961.0,"Position":328.0,"HyperDash":false}]},{"StartTime":55348.0,"Objects":[{"StartTime":55348.0,"Position":224.0,"HyperDash":false},{"StartTime":55444.0,"Position":195.951981,"HyperDash":false},{"StartTime":55541.0,"Position":173.643036,"HyperDash":false},{"StartTime":55620.0,"Position":140.030609,"HyperDash":false},{"StartTime":55735.0,"Position":123.025146,"HyperDash":false}]},{"StartTime":55929.0,"Objects":[{"StartTime":55929.0,"Position":72.0,"HyperDash":false},{"StartTime":56025.0,"Position":95.8109741,"HyperDash":false},{"StartTime":56122.0,"Position":121.880394,"HyperDash":false},{"StartTime":56201.0,"Position":145.29776,"HyperDash":false},{"StartTime":56316.0,"Position":172.019226,"HyperDash":false}]},{"StartTime":56509.0,"Objects":[{"StartTime":56509.0,"Position":256.0,"HyperDash":false}]},{"StartTime":56896.0,"Objects":[{"StartTime":56896.0,"Position":328.0,"HyperDash":false},{"StartTime":56992.0,"Position":356.5922,"HyperDash":false},{"StartTime":57089.0,"Position":368.955872,"HyperDash":false},{"StartTime":57186.0,"Position":373.612579,"HyperDash":false},{"StartTime":57283.0,"Position":419.1303,"HyperDash":false},{"StartTime":57361.0,"Position":444.262817,"HyperDash":false},{"StartTime":57476.0,"Position":466.641,"HyperDash":false}]},{"StartTime":57671.0,"Objects":[{"StartTime":57671.0,"Position":416.0,"HyperDash":false},{"StartTime":57767.0,"Position":391.6712,"HyperDash":false},{"StartTime":57864.0,"Position":367.089,"HyperDash":false},{"StartTime":57943.0,"Position":328.06842,"HyperDash":false},{"StartTime":58058.0,"Position":317.924561,"HyperDash":false}]},{"StartTime":58445.0,"Objects":[{"StartTime":58445.0,"Position":144.0,"HyperDash":false}]},{"StartTime":58832.0,"Objects":[{"StartTime":58832.0,"Position":320.0,"HyperDash":false}]},{"StartTime":59219.0,"Objects":[{"StartTime":59219.0,"Position":128.0,"HyperDash":false}]},{"StartTime":59606.0,"Objects":[{"StartTime":59606.0,"Position":112.0,"HyperDash":false}]},{"StartTime":59993.0,"Objects":[{"StartTime":59993.0,"Position":224.0,"HyperDash":false},{"StartTime":60089.0,"Position":217.725632,"HyperDash":false},{"StartTime":60186.0,"Position":227.34639,"HyperDash":false},{"StartTime":60265.0,"Position":214.703522,"HyperDash":false},{"StartTime":60380.0,"Position":206.754791,"HyperDash":false}]},{"StartTime":60767.0,"Objects":[{"StartTime":60767.0,"Position":80.0,"HyperDash":false},{"StartTime":60863.0,"Position":90.79409,"HyperDash":false},{"StartTime":60960.0,"Position":75.87808,"HyperDash":false},{"StartTime":61039.0,"Position":74.4997559,"HyperDash":false},{"StartTime":61154.0,"Position":96.53038,"HyperDash":false}]},{"StartTime":61542.0,"Objects":[{"StartTime":61542.0,"Position":200.0,"HyperDash":false},{"StartTime":61638.0,"Position":196.089508,"HyperDash":false},{"StartTime":61735.0,"Position":236.445679,"HyperDash":false},{"StartTime":61814.0,"Position":270.5239,"HyperDash":false},{"StartTime":61929.0,"Position":286.4193,"HyperDash":false}]},{"StartTime":62316.0,"Objects":[{"StartTime":62316.0,"Position":376.0,"HyperDash":false},{"StartTime":62412.0,"Position":361.9105,"HyperDash":false},{"StartTime":62509.0,"Position":339.554321,"HyperDash":false},{"StartTime":62588.0,"Position":316.4761,"HyperDash":false},{"StartTime":62703.0,"Position":289.5807,"HyperDash":false}]},{"StartTime":63090.0,"Objects":[{"StartTime":63090.0,"Position":184.0,"HyperDash":false},{"StartTime":63186.0,"Position":174.5783,"HyperDash":false},{"StartTime":63283.0,"Position":191.193848,"HyperDash":false},{"StartTime":63362.0,"Position":204.138489,"HyperDash":false},{"StartTime":63477.0,"Position":198.424973,"HyperDash":false}]},{"StartTime":63864.0,"Objects":[{"StartTime":63864.0,"Position":88.0,"HyperDash":false},{"StartTime":63960.0,"Position":45.16764,"HyperDash":false},{"StartTime":64057.0,"Position":38.0766068,"HyperDash":false},{"StartTime":64154.0,"Position":12.98558,"HyperDash":false},{"StartTime":64251.0,"Position":38.0766144,"HyperDash":false},{"StartTime":64329.0,"Position":69.2529,"HyperDash":false},{"StartTime":64444.0,"Position":88.0,"HyperDash":false}]},{"StartTime":64638.0,"Objects":[{"StartTime":64638.0,"Position":312.0,"HyperDash":false}]},{"StartTime":64832.0,"Objects":[{"StartTime":64832.0,"Position":208.0,"HyperDash":false}]},{"StartTime":65025.0,"Objects":[{"StartTime":65025.0,"Position":304.0,"HyperDash":false}]},{"StartTime":65413.0,"Objects":[{"StartTime":65413.0,"Position":360.0,"HyperDash":false},{"StartTime":65491.0,"Position":361.6114,"HyperDash":false},{"StartTime":65606.0,"Position":411.0,"HyperDash":false}]},{"StartTime":65800.0,"Objects":[{"StartTime":65800.0,"Position":462.0,"HyperDash":false},{"StartTime":65878.0,"Position":458.3886,"HyperDash":false},{"StartTime":65993.0,"Position":411.0,"HyperDash":false}]},{"StartTime":66187.0,"Objects":[{"StartTime":66187.0,"Position":344.0,"HyperDash":false},{"StartTime":66283.0,"Position":327.697662,"HyperDash":false},{"StartTime":66380.0,"Position":293.131775,"HyperDash":false},{"StartTime":66459.0,"Position":271.3101,"HyperDash":false},{"StartTime":66574.0,"Position":242.0,"HyperDash":false}]},{"StartTime":66961.0,"Objects":[{"StartTime":66961.0,"Position":152.0,"HyperDash":false},{"StartTime":67057.0,"Position":167.659241,"HyperDash":false},{"StartTime":67154.0,"Position":147.9835,"HyperDash":false},{"StartTime":67233.0,"Position":135.201309,"HyperDash":false},{"StartTime":67348.0,"Position":106.616547,"HyperDash":false}]},{"StartTime":67735.0,"Objects":[{"StartTime":67735.0,"Position":32.0,"HyperDash":false},{"StartTime":67831.0,"Position":42.2565079,"HyperDash":false},{"StartTime":67928.0,"Position":78.75527,"HyperDash":false},{"StartTime":68007.0,"Position":85.89344,"HyperDash":false},{"StartTime":68122.0,"Position":125.752792,"HyperDash":false}]},{"StartTime":68316.0,"Objects":[{"StartTime":68316.0,"Position":208.0,"HyperDash":false}]},{"StartTime":68509.0,"Objects":[{"StartTime":68509.0,"Position":224.0,"HyperDash":false},{"StartTime":68605.0,"Position":240.243561,"HyperDash":false},{"StartTime":68702.0,"Position":270.729248,"HyperDash":false},{"StartTime":68781.0,"Position":291.85675,"HyperDash":false},{"StartTime":68896.0,"Position":317.7006,"HyperDash":false}]},{"StartTime":69284.0,"Objects":[{"StartTime":69284.0,"Position":216.0,"HyperDash":false},{"StartTime":69380.0,"Position":191.846649,"HyperDash":false},{"StartTime":69477.0,"Position":185.704849,"HyperDash":false},{"StartTime":69556.0,"Position":168.950912,"HyperDash":false},{"StartTime":69671.0,"Position":193.879364,"HyperDash":false}]},{"StartTime":70058.0,"Objects":[{"StartTime":70058.0,"Position":360.0,"HyperDash":false},{"StartTime":70154.0,"Position":374.986725,"HyperDash":false},{"StartTime":70251.0,"Position":367.9918,"HyperDash":false},{"StartTime":70330.0,"Position":353.6845,"HyperDash":false},{"StartTime":70445.0,"Position":337.885529,"HyperDash":false}]},{"StartTime":70832.0,"Objects":[{"StartTime":70832.0,"Position":264.0,"HyperDash":false},{"StartTime":70928.0,"Position":225.951981,"HyperDash":false},{"StartTime":71025.0,"Position":213.643036,"HyperDash":false},{"StartTime":71104.0,"Position":187.030609,"HyperDash":false},{"StartTime":71219.0,"Position":163.025146,"HyperDash":false}]},{"StartTime":71413.0,"Objects":[{"StartTime":71413.0,"Position":112.0,"HyperDash":false},{"StartTime":71509.0,"Position":75.97218,"HyperDash":false},{"StartTime":71606.0,"Position":61.6836624,"HyperDash":false},{"StartTime":71685.0,"Position":27.0878525,"HyperDash":false},{"StartTime":71800.0,"Position":11.1066208,"HyperDash":false}]},{"StartTime":71993.0,"Objects":[{"StartTime":71993.0,"Position":40.0,"HyperDash":false},{"StartTime":72071.0,"Position":52.4331474,"HyperDash":false},{"StartTime":72186.0,"Position":68.28971,"HyperDash":false}]},{"StartTime":72380.0,"Objects":[{"StartTime":72380.0,"Position":176.0,"HyperDash":false},{"StartTime":72476.0,"Position":187.970581,"HyperDash":false},{"StartTime":72573.0,"Position":161.344147,"HyperDash":false},{"StartTime":72670.0,"Position":136.575012,"HyperDash":false},{"StartTime":72767.0,"Position":119.0057,"HyperDash":false},{"StartTime":72845.0,"Position":79.55367,"HyperDash":false},{"StartTime":72960.0,"Position":69.6823959,"HyperDash":false}]},{"StartTime":73154.0,"Objects":[{"StartTime":73154.0,"Position":120.0,"HyperDash":false},{"StartTime":73250.0,"Position":160.2306,"HyperDash":false},{"StartTime":73347.0,"Position":169.814178,"HyperDash":false},{"StartTime":73426.0,"Position":170.964127,"HyperDash":false},{"StartTime":73541.0,"Position":209.453659,"HyperDash":false}]},{"StartTime":73929.0,"Objects":[{"StartTime":73929.0,"Position":312.0,"HyperDash":false},{"StartTime":74025.0,"Position":321.048035,"HyperDash":false},{"StartTime":74122.0,"Position":362.356964,"HyperDash":false},{"StartTime":74201.0,"Position":394.9694,"HyperDash":false},{"StartTime":74316.0,"Position":412.974854,"HyperDash":false}]},{"StartTime":74703.0,"Objects":[{"StartTime":74703.0,"Position":336.0,"HyperDash":false},{"StartTime":74781.0,"Position":342.880768,"HyperDash":false},{"StartTime":74896.0,"Position":315.910126,"HyperDash":false}]},{"StartTime":75090.0,"Objects":[{"StartTime":75090.0,"Position":400.0,"HyperDash":false},{"StartTime":75168.0,"Position":399.237152,"HyperDash":false},{"StartTime":75283.0,"Position":417.9073,"HyperDash":false}]},{"StartTime":75477.0,"Objects":[{"StartTime":75477.0,"Position":328.0,"HyperDash":false},{"StartTime":75573.0,"Position":296.892883,"HyperDash":false},{"StartTime":75670.0,"Position":288.19165,"HyperDash":false},{"StartTime":75767.0,"Position":275.9188,"HyperDash":false},{"StartTime":75864.0,"Position":238.263733,"HyperDash":false},{"StartTime":75942.0,"Position":221.022842,"HyperDash":false},{"StartTime":76057.0,"Position":202.919159,"HyperDash":false}]},{"StartTime":76251.0,"Objects":[{"StartTime":76251.0,"Position":296.0,"HyperDash":false},{"StartTime":76347.0,"Position":313.798157,"HyperDash":false},{"StartTime":76444.0,"Position":346.261322,"HyperDash":false},{"StartTime":76523.0,"Position":375.263733,"HyperDash":false},{"StartTime":76638.0,"Position":392.493958,"HyperDash":false}]},{"StartTime":77219.0,"Objects":[{"StartTime":77219.0,"Position":152.0,"HyperDash":false},{"StartTime":77315.0,"Position":119.697678,"HyperDash":false},{"StartTime":77412.0,"Position":101.0,"HyperDash":false},{"StartTime":77491.0,"Position":137.689911,"HyperDash":false},{"StartTime":77606.0,"Position":152.0,"HyperDash":false}]},{"StartTime":77800.0,"Objects":[{"StartTime":77800.0,"Position":320.0,"HyperDash":false}]},{"StartTime":78187.0,"Objects":[{"StartTime":78187.0,"Position":320.0,"HyperDash":false},{"StartTime":78265.0,"Position":323.92218,"HyperDash":false},{"StartTime":78380.0,"Position":369.294647,"HyperDash":false}]},{"StartTime":78574.0,"Objects":[{"StartTime":78574.0,"Position":456.0,"HyperDash":false},{"StartTime":78670.0,"Position":443.684448,"HyperDash":false},{"StartTime":78767.0,"Position":433.251038,"HyperDash":false},{"StartTime":78846.0,"Position":411.9393,"HyperDash":false},{"StartTime":78961.0,"Position":410.384216,"HyperDash":false}]},{"StartTime":79348.0,"Objects":[{"StartTime":79348.0,"Position":288.0,"HyperDash":false},{"StartTime":79444.0,"Position":287.315552,"HyperDash":false},{"StartTime":79541.0,"Position":310.748962,"HyperDash":false},{"StartTime":79620.0,"Position":322.0607,"HyperDash":false},{"StartTime":79735.0,"Position":333.615784,"HyperDash":false}]},{"StartTime":80122.0,"Objects":[{"StartTime":80122.0,"Position":240.0,"HyperDash":false},{"StartTime":80218.0,"Position":206.699463,"HyperDash":false},{"StartTime":80315.0,"Position":192.5893,"HyperDash":false},{"StartTime":80394.0,"Position":180.9993,"HyperDash":false},{"StartTime":80509.0,"Position":144.773514,"HyperDash":false}]},{"StartTime":80703.0,"Objects":[{"StartTime":80703.0,"Position":64.0,"HyperDash":false}]},{"StartTime":80896.0,"Objects":[{"StartTime":80896.0,"Position":40.0,"HyperDash":false},{"StartTime":80992.0,"Position":52.30054,"HyperDash":false},{"StartTime":81089.0,"Position":87.4107056,"HyperDash":false},{"StartTime":81168.0,"Position":99.0006943,"HyperDash":false},{"StartTime":81283.0,"Position":135.226486,"HyperDash":false}]},{"StartTime":81671.0,"Objects":[{"StartTime":81671.0,"Position":248.0,"HyperDash":false},{"StartTime":81767.0,"Position":248.315552,"HyperDash":false},{"StartTime":81864.0,"Position":270.748962,"HyperDash":false},{"StartTime":81943.0,"Position":270.0607,"HyperDash":false},{"StartTime":82058.0,"Position":293.615784,"HyperDash":false}]},{"StartTime":82445.0,"Objects":[{"StartTime":82445.0,"Position":120.0,"HyperDash":false}]},{"StartTime":82832.0,"Objects":[{"StartTime":82832.0,"Position":312.0,"HyperDash":false}]},{"StartTime":83219.0,"Objects":[{"StartTime":83219.0,"Position":400.0,"HyperDash":false},{"StartTime":83297.0,"Position":406.999,"HyperDash":false},{"StartTime":83412.0,"Position":412.369324,"HyperDash":false}]},{"StartTime":83606.0,"Objects":[{"StartTime":83606.0,"Position":360.0,"HyperDash":false},{"StartTime":83684.0,"Position":344.762848,"HyperDash":false},{"StartTime":83799.0,"Position":342.0927,"HyperDash":false}]},{"StartTime":83993.0,"Objects":[{"StartTime":83993.0,"Position":272.0,"HyperDash":false},{"StartTime":84089.0,"Position":267.17865,"HyperDash":false},{"StartTime":84186.0,"Position":223.813,"HyperDash":false},{"StartTime":84265.0,"Position":218.85318,"HyperDash":false},{"StartTime":84380.0,"Position":179.797424,"HyperDash":false}]},{"StartTime":84767.0,"Objects":[{"StartTime":84767.0,"Position":80.0,"HyperDash":false},{"StartTime":84845.0,"Position":91.5179,"HyperDash":false},{"StartTime":84960.0,"Position":96.12762,"HyperDash":false}]},{"StartTime":85154.0,"Objects":[{"StartTime":85154.0,"Position":16.0,"HyperDash":false},{"StartTime":85232.0,"Position":0.0,"HyperDash":false},{"StartTime":85347.0,"Position":16.0,"HyperDash":false}]},{"StartTime":85542.0,"Objects":[{"StartTime":85542.0,"Position":104.0,"HyperDash":false},{"StartTime":85638.0,"Position":112.302322,"HyperDash":false},{"StartTime":85735.0,"Position":154.868225,"HyperDash":false},{"StartTime":85814.0,"Position":182.689911,"HyperDash":false},{"StartTime":85929.0,"Position":206.0,"HyperDash":false}]},{"StartTime":86316.0,"Objects":[{"StartTime":86316.0,"Position":376.0,"HyperDash":false},{"StartTime":86412.0,"Position":382.0039,"HyperDash":false},{"StartTime":86509.0,"Position":424.2578,"HyperDash":false},{"StartTime":86588.0,"Position":445.011047,"HyperDash":false},{"StartTime":86703.0,"Position":472.7657,"HyperDash":false}]},{"StartTime":87090.0,"Objects":[{"StartTime":87090.0,"Position":296.0,"HyperDash":false},{"StartTime":87186.0,"Position":311.353729,"HyperDash":false},{"StartTime":87283.0,"Position":308.602417,"HyperDash":false},{"StartTime":87362.0,"Position":307.240021,"HyperDash":false},{"StartTime":87477.0,"Position":312.385956,"HyperDash":false}]},{"StartTime":87864.0,"Objects":[{"StartTime":87864.0,"Position":24.0,"HyperDash":false}]},{"StartTime":88251.0,"Objects":[{"StartTime":88251.0,"Position":249.0,"HyperDash":false},{"StartTime":88347.0,"Position":233.0,"HyperDash":false},{"StartTime":88444.0,"Position":449.0,"HyperDash":false},{"StartTime":88541.0,"Position":411.0,"HyperDash":false},{"StartTime":88638.0,"Position":75.0,"HyperDash":false},{"StartTime":88735.0,"Position":474.0,"HyperDash":false},{"StartTime":88831.0,"Position":176.0,"HyperDash":false},{"StartTime":88928.0,"Position":1.0,"HyperDash":false},{"StartTime":89025.0,"Position":37.0,"HyperDash":false},{"StartTime":89122.0,"Position":481.0,"HyperDash":false},{"StartTime":89219.0,"Position":375.0,"HyperDash":false},{"StartTime":89315.0,"Position":407.0,"HyperDash":false},{"StartTime":89412.0,"Position":231.0,"HyperDash":false},{"StartTime":89509.0,"Position":338.0,"HyperDash":false},{"StartTime":89606.0,"Position":322.0,"HyperDash":false},{"StartTime":89703.0,"Position":347.0,"HyperDash":false},{"StartTime":89800.0,"Position":365.0,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu new file mode 100644 index 0000000000..a0cecc1b18 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/1041052.osu @@ -0,0 +1,210 @@ +osu file format v14 + +[General] +AudioFilename: audio.mp3 +AudioLeadIn: 0 +PreviewTime: 65316 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 2 +LetterboxInBreaks: 0 +WidescreenStoryboard: 0 + +[Editor] +DistanceSpacing: 1.4 +BeatDivisor: 4 +GridSize: 8 +TimelineZoom: 1.4 + +[Metadata] +Title:Nanairo Symphony -TV Size- +TitleUnicode:七色シンフォニー -TV Size- +Artist:Coalamode. +ArtistUnicode:コアラモード. +Creator:Ascendance +Version:Aru's Cup +Source:四月は君の嘘 +Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar] +BeatmapID:1041052 +BeatmapSetID:488149 + +[Difficulty] +HPDrainRate:3 +CircleSize:2.5 +OverallDifficulty:6 +ApproachRate:6 +SliderMultiplier:1.02 +SliderTickRate:2 + +[Events] +//Background and Video events +Video,500,"forty.avi" +0,0,"cropped-1366-768-647733.jpg",0,0 +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +1155,387.096774193548,4,2,1,50,1,0 +15284,-100,4,2,1,60,0,0 +16638,-100,4,2,1,50,0,0 +41413,-100,4,2,1,60,0,0 +59993,-100,4,2,1,65,0,0 +66187,-100,4,2,1,70,0,1 +87284,-100,4,2,1,60,0,1 +87864,-100,4,2,1,70,0,0 +87961,-100,4,2,1,50,0,0 +88638,-100,4,2,1,30,0,0 +89413,-100,4,2,1,10,0,0 +89800,-100,4,2,1,5,0,0 + + +[Colours] +Combo1 : 255,128,64 +Combo2 : 0,128,255 +Combo3 : 255,128,192 +Combo4 : 0,128,192 + +[HitObjects] +208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0: +160,160,2122,1,0,0:0:0:0: +272,160,2509,1,2,0:0:0:0: +448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0: +384,96,4058,1,2,0:0:0:0: +128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +192,64,5800,1,2,0:0:0:0: +240,64,5993,1,2,0:0:0:0: +288,64,6187,1,2,0:0:0:0: +416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0: +488,160,8122,2,0,L|376:160,1,102 +457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0: +400,288,10058,1,0,0:0:0:0: +304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0: +400,288,11606,1,0,0:0:0:0: +240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0: +0,288,13154,1,0,0:0:0:0: +112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0: +288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0: +192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0: +312,208,15864,1,6,0:0:0:0: +128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0: +224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0: +128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0: +432,176,20896,1,0,0:0:0:0: +328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0: +328,176,22445,1,0,0:0:0:0: +224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0: +224,176,23993,1,0,0:0:0:0: +112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0: +416,176,25541,1,0,0:0:0:0: +304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0: +480,112,27090,1,0,0:0:0:0: +384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0: +432,112,28058,1,2,0:0:0:0: +333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0: +384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0: +224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0: +272,272,30574,2,0,L|374:272,1,102 +424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0: +224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0: +200,368,32703,1,2,0:0:0:0: +376,368,33284,1,0,0:0:0:0: +304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0: +352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0: +280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0: +272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0: +280,16,36380,1,2,0:0:0:0: +176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0: +168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0: +176,224,37928,1,2,0:0:0:0: +304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0: +144,264,38896,1,2,0:0:0:0: +280,336,39477,2,0,L|336:336,1,51 +424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0: +352,232,40445,1,4,0:1:0:0: +160,224,41025,1,8,0:3:0:0: +256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0: +400,40,41993,1,0,0:0:0:0: +440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0: +248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0: +144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0: +56,136,45284,1,2,0:0:0:0: +160,144,45671,1,8,0:0:0:0: +264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0: +416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0: +360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0: +160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0: +144,136,49154,1,2,0:0:0:0: +248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0: +256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0: +256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0: +400,208,51284,1,0,0:0:0:0: +352,240,51477,2,0,L|240:240,1,102 +128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0: +88,264,53025,1,2,0:0:0:0: +168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0: +248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0: +432,120,54574,1,2,0:0:0:0: +328,128,54961,1,8,0:0:0:0: +224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0: +72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0: +224,184,56509,1,8,0:3:0:0: +328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0: +416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0: +224,272,58445,5,2,0:0:0:0: +320,296,58832,1,0,0:0:0:0: +224,328,59219,1,2,0:0:0:0: +120,328,59606,1,8,0:3:0:0: +224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0: +80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0: +200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0: +376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0: +184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0: +88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0: +160,112,64638,1,8,0:0:0:0: +208,128,64832,1,8,0:0:0:0: +256,144,65025,1,8,0:0:0:0: +360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0: +462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0: +344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0: +152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0: +32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0: +176,208,68316,1,0,0:0:0:0: +224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0: +216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0: +360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0: +264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0: +112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0: +40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0: +176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0: +120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0: +312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0: +336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0: +400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0: +328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0: +296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0: +152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0: +208,248,77800,1,8,0:0:0:0: +320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0: +456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0: +288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0: +240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0: +96,72,80703,1,0,0:0:0:0: +40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0: +248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0: +208,272,82445,1,10,0:0:0:0: +312,272,82832,1,8,0:0:0:0: +400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0: +360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0: +272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0: +80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0: +16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0: +104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0: +376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0: +296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0: +168,96,87864,1,4,0:0:0:0: +256,192,88251,12,0,89800,0:0:0:0: From 285adcb00e65abbcaad8d6a7cffedcc416949ab5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Mar 2024 00:19:45 +0300 Subject: [PATCH 57/88] Fix catch hit object position getting randomised when last object has pos=0 --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 200018f28b..198f8f59c6 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps float offsetPosition = hitObject.OriginalX; double startTime = hitObject.StartTime; - if (lastPosition == null) + if (lastPosition == null || + // some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here. + // reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50 + // todo: should be revisited and corrected later probably. + lastPosition == 0) { lastPosition = offsetPosition; lastStartTime = startTime; From af2b80e0304e9541378fa9fa68caad0ca347a37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Mar 2024 11:28:34 +0100 Subject: [PATCH 58/88] Add failing encode test --- .../Formats/LegacyScoreDecoderTest.cs | 2 +- .../Formats/LegacyScoreEncoderTest.cs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 7e3967dc95..43e471320e 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats CultureInfo.CurrentCulture = originalCulture; } - private class TestLegacyScoreDecoder : LegacyScoreDecoder + public class TestLegacyScoreDecoder : LegacyScoreDecoder { private readonly int beatmapVersion; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs new file mode 100644 index 0000000000..c0a7285f39 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs @@ -0,0 +1,55 @@ +// 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 System.IO; +using NUnit.Framework; +using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + public class LegacyScoreEncoderTest + { + [TestCase(1, 3)] + [TestCase(1, 0)] + [TestCase(0, 3)] + public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount) + { + var ruleset = new CatchRuleset().RulesetInfo; + + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + scoreInfo.Statistics = new Dictionary + { + [HitResult.Great] = 50, + [HitResult.LargeTickHit] = 5, + [HitResult.Miss] = missCount, + [HitResult.LargeTickMiss] = largeTickMissCount + }; + var score = new Score { ScoreInfo = scoreInfo }; + + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount)); + } + + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) + { + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); + var decodedAfterEncode = decoder.Parse(decodeStream); + return decodedAfterEncode; + } + } +} From dcd6b028090a97ea1b2c43a374bb9210417b0adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Mar 2024 11:35:31 +0100 Subject: [PATCH 59/88] Fix incorrect implementation of `GetCountMiss()` for catch --- .../Scoring/Legacy/ScoreInfoExtensions.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 07c35a334f..23624401e2 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -198,10 +198,25 @@ namespace osu.Game.Scoring.Legacy } } - public static int? GetCountMiss(this ScoreInfo scoreInfo) => - getCount(scoreInfo, HitResult.Miss); + public static int? GetCountMiss(this ScoreInfo scoreInfo) + { + switch (scoreInfo.Ruleset.OnlineID) + { + case 0: + case 1: + case 3: + return getCount(scoreInfo, HitResult.Miss); + + case 2: + return (getCount(scoreInfo, HitResult.Miss) ?? 0) + (getCount(scoreInfo, HitResult.LargeTickMiss) ?? 0); + } + + return null; + } public static void SetCountMiss(this ScoreInfo scoreInfo, int value) => + // this does not match the implementation of `GetCountMiss()` for catch, + // but we physically cannot recover that data anymore at this point. scoreInfo.Statistics[HitResult.Miss] = value; private static int? getCount(ScoreInfo scoreInfo, HitResult result) From b896d97a4f844498e0c1c914a0b70c4fdf70449f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Mar 2024 11:45:44 +0100 Subject: [PATCH 60/88] Fix catch pp calculator not matching live with respect to miss handling --- .../Difficulty/CatchPerformanceCalculator.cs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index b30b85be2d..d07f25ba90 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -2,22 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchPerformanceCalculator : PerformanceCalculator { - private int fruitsHit; - private int ticksHit; - private int tinyTicksHit; - private int tinyTicksMissed; - private int misses; + private int num300; + private int num100; + private int num50; + private int numKatu; + private int numMiss; public CatchPerformanceCalculator() : base(new CatchRuleset()) @@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty { var catchAttributes = (CatchDifficultyAttributes)attributes; - fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great); - ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit); - tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit); - tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss); - misses = score.Statistics.GetValueOrDefault(HitResult.Miss); + num300 = score.GetCount300() ?? 0; // HitResult.Great + num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit + num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit + numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss + numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss // We are heavily relying on aim in catch the beat double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0); value *= lengthBonus; - value *= Math.Pow(0.97, misses); + value *= Math.Pow(0.97, numMiss); // Combo scaling if (catchAttributes.MaxCombo > 0) @@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty } private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1); - private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed; - private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit; - private int totalComboHits() => misses + ticksHit + fruitsHit; + private int totalHits() => num50 + num100 + num300 + numMiss + numKatu; + private int totalSuccessfulHits() => num50 + num100 + num300; + private int totalComboHits() => numMiss + num100 + num300; } } From 405958f73c560b5c3eae381255d5141e37c819c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Mar 2024 14:43:53 +0100 Subject: [PATCH 61/88] Add test scene for drawable ranks --- .../Visual/Ranking/TestSceneDrawableRank.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs b/osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs new file mode 100644 index 0000000000..804c8dfc44 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneDrawableRank : OsuTestScene + { + [Test] + public void TestAllRanks() + { + AddStep("create content", () => Child = new FillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(10), + ChildrenEnumerable = Enum.GetValues().OrderBy(v => v).Select(rank => new DrawableRank(rank) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(50, 25), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }) + }); + } + } +} From 6080c14dd5b589c982b09365aa1b5eb74ccd14ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Mar 2024 14:47:49 +0100 Subject: [PATCH 62/88] Update F rank badge colours to match latest designs --- osu.Game/Graphics/OsuColour.cs | 6 +++++- osu.Game/Online/Leaderboards/DrawableRank.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 985898958c..c479d0cfe4 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -63,8 +63,12 @@ namespace osu.Game.Graphics case ScoreRank.C: return Color4Extensions.FromHex(@"ff8e5d"); - default: + case ScoreRank.D: return Color4Extensions.FromHex(@"ff5a5a"); + + case ScoreRank.F: + default: + return Color4Extensions.FromHex(@"3f3f3f"); } } diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 5177f35478..0b0ab11410 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -95,8 +95,12 @@ namespace osu.Game.Online.Leaderboards case ScoreRank.C: return Color4Extensions.FromHex(@"473625"); - default: + case ScoreRank.D: return Color4Extensions.FromHex(@"512525"); + + case ScoreRank.F: + default: + return Color4Extensions.FromHex(@"CC3333"); } } } From 9543908c7ab23cc397522ccaa1cfdc97c9d791b1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Mar 2024 23:36:45 +0300 Subject: [PATCH 63/88] Fix mod select overlay settings order not always matching panels order --- .../TestSceneModSelectOverlay.cs | 24 +++++++++++++++++++ osu.Game/Overlays/Mods/ModSettingsArea.cs | 11 +++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b26e126249..6c75530a6e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -859,6 +859,30 @@ namespace osu.Game.Tests.Visual.UserInterface () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); } + [Test] + public void TestModSettingsOrder() + { + createScreen(); + + AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() }); + AddAssert("mod settings order: DT, HD, DF", () => + { + var columns = this.ChildrenOfType().Single().ChildrenOfType(); + return columns.ElementAt(0).Mod is OsuModDoubleTime && + columns.ElementAt(1).Mod is OsuModHidden && + columns.ElementAt(2).Mod is OsuModDeflate; + }); + + AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList()); + AddAssert("mod settings order: NC, HD, DF", () => + { + var columns = this.ChildrenOfType().Single().ChildrenOfType(); + return columns.ElementAt(0).Mod is OsuModNightcore && + columns.ElementAt(1).Mod is OsuModHidden && + columns.ElementAt(2).Mod is OsuModDeflate; + }); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded) diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index 54bfcc7199..d0e0f7e648 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -86,7 +86,10 @@ namespace osu.Game.Overlays.Mods { modSettingsFlow.Clear(); - foreach (var mod in SelectedMods.Value.AsOrdered()) + // Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels). + // Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent), + // which breaks user expectations when interacting with the overlay. + foreach (var mod in SelectedMods.Value) { var settings = mod.CreateSettingsControls().ToList(); @@ -110,10 +113,14 @@ namespace osu.Game.Overlays.Mods protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnHover(HoverEvent e) => true; - private partial class ModSettingsColumn : CompositeDrawable + public partial class ModSettingsColumn : CompositeDrawable { + public readonly Mod Mod; + public ModSettingsColumn(Mod mod, IEnumerable settingsControls) { + Mod = mod; + Width = 250; RelativeSizeAxes = Axes.Y; Padding = new MarginPadding { Bottom = 7 }; From 43841e210d2a1915d3c39bfe191ef62befbd3492 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 5 Mar 2024 09:58:46 +0100 Subject: [PATCH 64/88] Use ObjectDisposedException.ThrowIf throw helper --- osu.Game/Database/RealmAccess.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 4bd7f36cdd..167d170c81 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -489,8 +489,7 @@ namespace osu.Game.Database /// The work to run. public Task WriteAsync(Action action) { - if (isDisposed) - throw new ObjectDisposedException(nameof(RealmAccess)); + ObjectDisposedException.ThrowIf(isDisposed, this); // Required to ensure the write is tracked and accounted for before disposal. // Can potentially be avoided if we have a need to do so in the future. @@ -675,8 +674,7 @@ namespace osu.Game.Database private Realm getRealmInstance() { - if (isDisposed) - throw new ObjectDisposedException(nameof(RealmAccess)); + ObjectDisposedException.ThrowIf(isDisposed, this); bool tookSemaphoreLock = false; @@ -1189,8 +1187,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); - if (isDisposed) - throw new ObjectDisposedException(nameof(RealmAccess)); + ObjectDisposedException.ThrowIf(isDisposed, this); SynchronizationContext? syncContext = null; From 9bac60a98fc098cb4c6b1f118e61c5255bd5b235 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 5 Mar 2024 10:19:47 +0100 Subject: [PATCH 65/88] Use ArgumentNullException.ThrowIfNull in more places --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 13e28279e6..bdc0ff85ba 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -135,8 +135,7 @@ namespace osu.Game.Rulesets.UI protected DrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset) { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap), "Beatmap cannot be null."); + ArgumentNullException.ThrowIfNull(beatmap); if (!(beatmap is Beatmap tBeatmap)) throw new ArgumentException($"{GetType()} expected the beatmap to contain hitobjects of type {typeof(TObject)}.", nameof(beatmap)); From a89130348469e1b1ea8e025bb2c6cf4a85da51b3 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 5 Mar 2024 10:20:30 +0100 Subject: [PATCH 66/88] Use ArgumentOutOfRangeException throw helper methods --- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs | 6 +++--- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Beatmaps/Timing/TimeSignature.cs | 3 +-- osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs | 3 +-- osu.Game/Rulesets/Objects/Types/PathType.cs | 3 +-- osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs | 3 +-- osu.Game/Utils/LimitedCapacityQueue.cs | 3 +-- osu.Game/Utils/StatelessRNG.cs | 2 +- 8 files changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index 835c67ff19..9cc0a8c414 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -58,9 +58,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps private void applyStacking(Beatmap beatmap, int startIndex, int endIndex) { - if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be greater than {nameof(endIndex)}."); - if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be less than 0."); - if (endIndex < 0) throw new ArgumentOutOfRangeException(nameof(endIndex), $"{nameof(endIndex)} cannot be less than 0."); + ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); + ArgumentOutOfRangeException.ThrowIfNegative(startIndex); + ArgumentOutOfRangeException.ThrowIfNegative(endIndex); int extendedEndIndex = endIndex; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 448cfaf84c..3ead61f64a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -334,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// The from a selected to a target . private OsuDistanceSnapGrid createGrid(Func sourceSelector, int targetOffset = 1) { - if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); int sourceIndex = -1; diff --git a/osu.Game/Beatmaps/Timing/TimeSignature.cs b/osu.Game/Beatmaps/Timing/TimeSignature.cs index 7499a725dc..377a878631 100644 --- a/osu.Game/Beatmaps/Timing/TimeSignature.cs +++ b/osu.Game/Beatmaps/Timing/TimeSignature.cs @@ -23,8 +23,7 @@ namespace osu.Game.Beatmaps.Timing public TimeSignature(int numerator) { - if (numerator < 1) - throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive."); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numerator); Numerator = numerator; } diff --git a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs index 3dc2d133ba..17c3c51cfb 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs @@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils public ReverseQueue(int initialCapacity) { - if (initialCapacity <= 0) - throw new ArgumentOutOfRangeException(nameof(initialCapacity)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapacity); items = new T[initialCapacity]; capacity = initialCapacity; diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 23f1ccf0bc..6983484dce 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -40,8 +40,7 @@ namespace osu.Game.Rulesets.Objects.Types public static PathType BSpline(int degree) { - if (degree <= 0) - throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero."); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(degree); return new PathType { Type = SplineType.BSpline, Degree = degree }; } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index ed31bc8643..4abf0007a7 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -33,8 +33,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display in this row. public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) { - if (columnCount < 1) - throw new ArgumentOutOfRangeException(nameof(columnCount)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(columnCount); this.columnCount = columnCount; this.items = items.ToArray(); diff --git a/osu.Game/Utils/LimitedCapacityQueue.cs b/osu.Game/Utils/LimitedCapacityQueue.cs index d36aa8af2c..b96148d4a0 100644 --- a/osu.Game/Utils/LimitedCapacityQueue.cs +++ b/osu.Game/Utils/LimitedCapacityQueue.cs @@ -35,8 +35,7 @@ namespace osu.Game.Utils /// The number of items the queue can hold. public LimitedCapacityQueue(int capacity) { - if (capacity < 0) - throw new ArgumentOutOfRangeException(nameof(capacity)); + ArgumentOutOfRangeException.ThrowIfNegative(capacity); this.capacity = capacity; array = new T[capacity]; diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs index 3db632fc42..8833dcbbdb 100644 --- a/osu.Game/Utils/StatelessRNG.cs +++ b/osu.Game/Utils/StatelessRNG.cs @@ -58,7 +58,7 @@ namespace osu.Game.Utils /// public static int NextInt(int maxValue, int seed, int series = 0) { - if (maxValue <= 0) throw new ArgumentOutOfRangeException(nameof(maxValue)); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxValue); return (int)(NextULong(seed, series) % (ulong)maxValue); } From 5965db4fd7344822d116aa7bd23ad2fa2ebfa22e Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 5 Mar 2024 10:20:57 +0100 Subject: [PATCH 67/88] Use ArgumentException.ThrowIfNullOrEmpty throw helper --- osu.Game/IO/WrappedStorage.cs | 3 +-- osu.Game/Online/API/OAuth.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 95ff26db6a..5a8f4aef59 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -80,8 +80,7 @@ namespace osu.Game.IO public override Storage GetStorageForDirectory(string path) { - if (string.IsNullOrEmpty(path)) - throw new ArgumentException("Must be non-null and not empty string", nameof(path)); + ArgumentException.ThrowIfNullOrEmpty(path); if (!path.EndsWith(Path.DirectorySeparatorChar)) path += Path.DirectorySeparatorChar; diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 4829310870..b06d9c6586 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -39,8 +39,8 @@ namespace osu.Game.Online.API internal void AuthenticateWithLogin(string username, string password) { - if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); - if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); + ArgumentException.ThrowIfNullOrEmpty(username); + ArgumentException.ThrowIfNullOrEmpty(password); var accessTokenRequest = new AccessTokenRequestPassword(username, password) { From 6fabbe26166df10907358d5c58dc26f8cf17dbbe Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 5 Mar 2024 10:27:12 +0100 Subject: [PATCH 68/88] Use new ToDictionary() overload without delegates --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 6f321fd401..64caddb2fc 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -245,8 +245,8 @@ namespace osu.Game.Online.API.Requests.Responses RulesetID = score.RulesetID, Passed = score.Passed, 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), + Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(), + MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(), }; } } diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index 2c5b91f10f..afdcef1d21 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -42,8 +42,8 @@ namespace osu.Game.Scoring.Legacy { 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), + Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(), + MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(), ClientVersion = score.ClientVersion, }; } From d0f7ab3316584f92957cd738c57e6dfd85fb9fcc Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 5 Mar 2024 17:18:59 +0100 Subject: [PATCH 69/88] Revert some changes --- osu.Game/Online/API/OAuth.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index b06d9c6586..4829310870 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -39,8 +39,8 @@ namespace osu.Game.Online.API internal void AuthenticateWithLogin(string username, string password) { - ArgumentException.ThrowIfNullOrEmpty(username); - ArgumentException.ThrowIfNullOrEmpty(password); + if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); + if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); var accessTokenRequest = new AccessTokenRequestPassword(username, password) { From 57daaa7fed6b7c7021e0c85d83ff8e532f657c7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 04:17:15 +0800 Subject: [PATCH 70/88] Add logging for `GameplayClockContainer` starting or stopping --- osu.Game/Screens/Play/GameplayClockContainer.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c039d1e535..255877e0aa 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -122,8 +122,17 @@ namespace osu.Game.Screens.Play StopGameplayClock(); } - protected virtual void StartGameplayClock() => GameplayClock.Start(); - protected virtual void StopGameplayClock() => GameplayClock.Stop(); + protected virtual void StartGameplayClock() + { + Logger.Log($"{nameof(GameplayClockContainer)} started via call to {nameof(StartGameplayClock)}"); + GameplayClock.Start(); + } + + protected virtual void StopGameplayClock() + { + Logger.Log($"{nameof(GameplayClockContainer)} stopped via call to {nameof(StopGameplayClock)}"); + GameplayClock.Stop(); + } /// /// Resets this and the source to an initial state ready for gameplay. From 65ce4ca390925c43ebb8167c4aa19c15b8b46b01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 04:17:51 +0800 Subject: [PATCH 71/88] Never set `waitingOnFrames` if a replay is not attached --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 884310e44c..b49924762e 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime); isCatchingUp.Value = timeBehind > 200; - waitingOnFrames.Value = state == PlaybackState.NotValid; + waitingOnFrames.Value = hasReplayAttached && state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(referenceClock.Rate) * direction; From 6455c0583b5e607baeca7f584410bc63515aa619 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 10:19:40 +0800 Subject: [PATCH 72/88] Update usage of `CircularProgress.Current` --- .../Skinning/Argon/ArgonSpinnerProgressArc.cs | 6 +++--- .../Skinning/Argon/ArgonSpinnerRingArc.cs | 6 +++--- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 8 ++++++-- osu.Game/Overlays/Volume/VolumeMeter.cs | 6 +++--- .../Edit/Compose/Components/CircularDistanceSnapGrid.cs | 2 +- osu.Game/Screens/Edit/Timing/TapButton.cs | 6 +++--- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 7 ++++++- .../Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 6 +++--- .../Screens/Ranking/Expanded/Accuracy/GradedCircles.cs | 2 +- osu.Game/Skinning/LegacySongProgress.cs | 4 ++-- 10 files changed, 31 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs index 76afeeb2c4..1de5b1f309 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Origin = Anchor.Centre, Colour = Color4.White.Opacity(0.25f), RelativeSizeAxes = Axes.Both, - Current = { Value = arc_fill }, + Progress = arc_fill, Rotation = 90 - arc_fill * 180, InnerRadius = arc_radius, RoundedCaps = true, @@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon background.Alpha = spinner.Progress >= 1 ? 0 : 1; fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed)); - fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed)); + fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed)); - fill.Rotation = (float)(90 - fill.Current.Value * 180); + fill.Rotation = (float)(90 - fill.Progress * 180); } private partial class ProgressFill : CircularProgress diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs index 702c5c2675..12cd0994b4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Current = { Value = arc_fill }, + Progress = arc_fill, Rotation = -arc_fill * 180, InnerRadius = arc_radius, RoundedCaps = true, @@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { base.Update(); - fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed)); + fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed)); fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed)); - fill.Rotation = (float)(-fill.Current.Value * 180); + fill.Rotation = (float)(-fill.Progress * 180); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 5a26a988fb..cd498c474a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -86,11 +86,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards Dimmed.BindValueChanged(_ => updateState()); playButton.Playing.BindValueChanged(_ => updateState(), true); - ((IBindable)progress.Current).BindTo(playButton.Progress); - FinishTransforms(true); } + protected override void Update() + { + base.Update(); + progress.Progress = playButton.Progress.Value; + } + private void updateState() { bool shouldDim = Dimmed.Value || playButton.Playing.Value; diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 6ec4971f06..e96cd0fa46 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Volume Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true); - bgProgress.Current.Value = 0.75f; + bgProgress.Progress = 0.75f; } private int? displayVolumeInt; @@ -265,8 +265,8 @@ namespace osu.Game.Overlays.Volume text.Text = intValue.ToString(CultureInfo.CurrentCulture); } - volumeCircle.Current.Value = displayVolume * 0.75f; - volumeCircleGlow.Current.Value = displayVolume * 0.75f; + volumeCircle.Progress = displayVolume * 0.75f; + volumeCircleGlow.Progress = displayVolume * 0.75f; if (intVolumeChanged && IsLoaded) Scheduler.AddOnce(playTickSound); diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index e33ef66007..92fe52148c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Colour = this.baseColour = baseColour; - Current.Value = 1; + Progress = 1; } protected override void Update() diff --git a/osu.Game/Screens/Edit/Timing/TapButton.cs b/osu.Game/Screens/Edit/Timing/TapButton.cs index fd60fb1b5b..d2ae0e76cf 100644 --- a/osu.Game/Screens/Edit/Timing/TapButton.cs +++ b/osu.Game/Screens/Edit/Timing/TapButton.cs @@ -366,7 +366,7 @@ namespace osu.Game.Screens.Edit.Timing new CircularProgress { RelativeSizeAxes = Axes.Both, - Current = { Value = 1f / light_count - angular_light_gap }, + Progress = 1f / light_count - angular_light_gap, Colour = colourProvider.Background2, }, fillContent = new Container @@ -379,7 +379,7 @@ namespace osu.Game.Screens.Edit.Timing new CircularProgress { RelativeSizeAxes = Axes.Both, - Current = { Value = 1f / light_count - angular_light_gap }, + Progress = 1f / light_count - angular_light_gap, Blending = BlendingParameters.Additive }, // Please do not try and make sense of this. @@ -388,7 +388,7 @@ namespace osu.Game.Screens.Edit.Timing Glow = new CircularProgress { RelativeSizeAxes = Axes.Both, - Current = { Value = 1f / light_count - 0.01f }, + Progress = 1f / light_count - 0.01f, Blending = BlendingParameters.Additive }.WithEffect(new GlowEffect { diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index a260156595..6d045e5f01 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -198,9 +198,14 @@ namespace osu.Game.Screens.Play.HUD bind(); } + protected override void Update() + { + base.Update(); + circularProgress.Progress = Progress.Value; + } + private void bind() { - ((IBindable)circularProgress.Current).BindTo(Progress); Progress.ValueChanged += progress => { icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 83b02a0951..2231346404 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Colour = OsuColour.Gray(47), Alpha = 0.5f, InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle - Current = { Value = 1 }, + Progress = 1, }, accuracyCircle = new CircularProgress { @@ -268,7 +268,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset) targetAccuracy -= visual_alignment_offset; - accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); + accuracyCircle.ProgressTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); if (withFlair) { @@ -359,7 +359,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy .FadeOut(800, Easing.Out); accuracyCircle - .FillTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); + .ProgressTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); badges.Single(b => b.Rank == getRank(ScoreRank.S)) .FadeOut(70, Easing.OutQuint); diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/GradedCircles.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/GradedCircles.cs index 33b71c53a7..633ed6d92e 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/GradedCircles.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/GradedCircles.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { public double RevealProgress { - set => Current.Value = Math.Clamp(value, startProgress, endProgress) - startProgress; + set => Progress = Math.Clamp(value, startProgress, endProgress) - startProgress; } private readonly double startProgress; diff --git a/osu.Game/Skinning/LegacySongProgress.cs b/osu.Game/Skinning/LegacySongProgress.cs index 4295060a3a..9af82c4992 100644 --- a/osu.Game/Skinning/LegacySongProgress.cs +++ b/osu.Game/Skinning/LegacySongProgress.cs @@ -72,14 +72,14 @@ namespace osu.Game.Skinning circularProgress.Scale = new Vector2(-1, 1); circularProgress.Anchor = Anchor.TopRight; circularProgress.Colour = new Colour4(199, 255, 47, 153); - circularProgress.Current.Value = 1 - progress; + circularProgress.Progress = 1 - progress; } else { circularProgress.Scale = new Vector2(1); circularProgress.Anchor = Anchor.TopLeft; circularProgress.Colour = new Colour4(255, 255, 255, 153); - circularProgress.Current.Value = progress; + circularProgress.Progress = progress; } } } From b53b752e543d563b1059cc66d6dd8c6077bb6e01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 10:42:20 +0800 Subject: [PATCH 73/88] Update usage of `MathUtils` --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- .../Objects/Drawables/DrawableSliderRepeat.cs | 2 +- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 2 +- .../Skinning/Default/SpinnerRotationTracker.cs | 2 +- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 3 +-- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 2 +- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 4 +--- osu.Game/IO/Archives/ZipArchiveReader.cs | 3 +-- .../Overlays/Settings/Sections/Input/TabletAreaSelection.cs | 3 +-- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 4 ++-- osu.Game/Utils/GeometryUtils.cs | 5 ++--- 13 files changed, 15 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index df9544b71e..992f4d5f03 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods // multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact, // some spinners may not complete due to very minor decimal loss during calculation float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); - spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); + spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 3239565528..fcbd0edfe0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } - float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); + float aimRotation = float.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); while (Math.Abs(aimRotation - Arrow.Rotation) > 180) aimRotation += aimRotation < Arrow.Rotation ? 360 : -360; diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 1cf6bc91f0..d43e6092c2 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays // 0.05 rad/ms, or ~477 RPM, as per stable. // the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs. const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000; - float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000; + float radsPerMillisecond = float.DegreesToRadians(spin_rpm * 360) / 60000; switch (h) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 1d75663fd9..7e97f826f9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (mousePosition is Vector2 pos) { - float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); + float thisAngle = -float.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; // Normalise the delta to -180 .. 180 diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f9d4a3b325..4b3b543ea4 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -246,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted. // // We also need to apply the anti-clockwise rotation. - double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation); + double rotatedAngle = finalAngle - float.DegreesToRadians(rotation); var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2; diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index cf4700bf85..6689f087cb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; using osu.Game.IO; using osu.Game.Storyboards; @@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue)); + timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue)); break; } diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 7e42d45191..696ea62b42 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -157,7 +157,7 @@ namespace osu.Game.Graphics.Cursor if (dragRotationState == DragRotationState.Rotating && distance > 0) { Vector2 offset = e.MousePosition - positionMouseDown; - float degrees = MathUtils.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f; + float degrees = float.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f; // Always rotate in the direction of least distance float diff = (degrees - activeCursor.Rotation) % 360; diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index df92863797..e9b28f4771 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions; - namespace osu.Game.Graphics.UserInterface { public partial class OsuNumberBox : OsuTextBox { protected override bool AllowIme => false; - protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); + protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); } } diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 5ef03b3641..7d7ce858dd 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Toolkit.HighPerformance; -using osu.Framework.Extensions; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; using SixLabors.ImageSharp.Memory; @@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives var owner = MemoryAllocator.Default.Allocate((int)entry.Size); using (Stream s = entry.OpenEntryStream()) - s.ReadToFill(owner.Memory.Span); + s.ReadExactly(owner.Memory.Span); return new MemoryOwnerMemoryStream(owner); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index 686002fe71..33f4f49173 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Input.Handlers.Tablet; -using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -196,7 +195,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input var matrix = Matrix3.Identity; MatrixExtensions.TranslateFromLeft(ref matrix, offset); - MatrixExtensions.RotateFromLeft(ref matrix, MathUtils.DegreesToRadians(rotation.Value)); + MatrixExtensions.RotateFromLeft(ref matrix, float.DegreesToRadians(rotation.Value)); usableAreaQuad *= matrix; diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index a0f85eda31..cdf648fc5f 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -69,7 +69,7 @@ namespace osu.Game.Overlays.Settings { protected override bool AllowIme => false; - protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); + protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); public new void NotifyInputError() => base.NotifyInputError(); } diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index b722b83280..c47ce91711 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -209,13 +209,13 @@ namespace osu.Game.Screens.Menu if (audioData[i] < amplitude_dead_zone) continue; - float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); + float rotation = float.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); float rotationCos = MathF.Cos(rotation); float rotationSin = MathF.Sin(rotation); // taking the cos and sin to the 0..1 range var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; - var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); + var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(float.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); // The distance between the position and the sides of the bar. var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); // The distance between the bottom side of the bar and the top side. diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 725e93d098..dbeba4dfc1 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -28,8 +27,8 @@ namespace osu.Game.Utils point.Y -= origin.Y; Vector2 ret; - ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); - ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); + ret.X = point.X * MathF.Cos(float.DegreesToRadians(angle)) + point.Y * MathF.Sin(float.DegreesToRadians(angle)); + ret.Y = point.X * -MathF.Sin(float.DegreesToRadians(angle)) + point.Y * MathF.Cos(float.DegreesToRadians(angle)); ret.X += origin.X; ret.Y += origin.Y; From 0696e7de524123d1b332a166c4cd81f5d6c24c15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 10:42:42 +0800 Subject: [PATCH 74/88] Update ImageSharp usages --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs | 2 +- osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs | 2 +- osu.Game/Skinning/LegacyTextureLoaderStore.cs | 2 +- osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 53a4abdd07..3f78dedec5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TextureUpload upscale(TextureUpload textureUpload) { - var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height); // The original texture upload will no longer be returned or used. textureUpload.Dispose(); diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs index 128e100e4b..cf58ae73fe 100644 --- a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs +++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs @@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps // The original texture upload will no longer be returned or used. textureUpload.Dispose(); - Size size = image.Size(); + Size size = image.Size; // Assume that panel backgrounds are always displayed using `FillMode.Fill`. // Also assume that all backgrounds are wider than they are tall, so the diff --git a/osu.Game/Skinning/LegacyTextureLoaderStore.cs b/osu.Game/Skinning/LegacyTextureLoaderStore.cs index 29206bbb85..5045374c14 100644 --- a/osu.Game/Skinning/LegacyTextureLoaderStore.cs +++ b/osu.Game/Skinning/LegacyTextureLoaderStore.cs @@ -73,7 +73,7 @@ namespace osu.Game.Skinning private TextureUpload convertToGrayscale(TextureUpload textureUpload) { - var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height); // stable uses `0.299 * r + 0.587 * g + 0.114 * b` // (https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/pTexture.cs#L138-L153) diff --git a/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs b/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs index f15097a169..58dadbe753 100644 --- a/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs +++ b/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning if (textureUpload.Height > max_supported_texture_size || textureUpload.Width > max_supported_texture_size) { - var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height); // The original texture upload will no longer be returned or used. textureUpload.Dispose(); From 4c7678225ee018e24724024fe88580c024164b55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 12:13:41 +0800 Subject: [PATCH 75/88] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1b395a7c83..4901f30d8a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 747d6059da..6b63bfa1e2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 3a224211aa322a0055342f10bd36e0af3c3b078c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 12:17:00 +0800 Subject: [PATCH 76/88] Update various packages which shouldn't cause issues --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- osu.Game/osu.Game.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 64da5412a8..af84ee47f1 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,7 +7,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 942763e388..b143a3a6b1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,8 +20,8 @@ - - + + @@ -35,14 +35,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + From 53fffc6a75df70e82be5c1e1a3d2fe633f86c834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Mar 2024 07:57:59 +0100 Subject: [PATCH 77/88] Remove unused using directives --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 1 - osu.Game/Screens/Menu/LogoVisualisation.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index cdf648fc5f..fbcdb4a968 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index c47ce91711..6d9d2f69b7 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osuTK; using osuTK.Graphics; From 08609e19d6bb13669cb4f44e0ae6b80a7444c786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Mar 2024 11:40:10 +0100 Subject: [PATCH 78/88] Fix osu! standardised score estimation algorithm violating basic invariants As it turns out, the "lower" and "upper" estimates of the combo portion of the score being converted were misnomers. In selected cases (scores with high accuracy but combo being lower than max by more than a few objects) the janky score-based math could overestimate the count of remaining objects in a map. For instance, in one case the numbers worked out something like this: - Accuracy: practically 100% - Max combo on beatmap: 571x - Max combo for score: 551x The score-based estimation attempts to extract a "remaining object count" from score, by doing something along of sqrt(571^2 - 551^2). That comes out to _almost 150_. Which leads to the estimation overshooting the total max combo count on the beatmap by some hundred objects. To curtail this nonsense, enforce some basic invariants: - Neither estimate is allowed to exceed maximum achievable - Ensure that lower estimate is really lower and upper is really upper by just looking at the values and making sure that is so rather than just saying that it is. --- .../Database/StandardisedScoreMigrationTools.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 0594c80390..53ff1a25ca 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -415,7 +415,7 @@ namespace osu.Game.Database // Calculate how many times the longest combo the user has achieved in the play can repeat // without exceeding the combo portion in score V1 as achieved by the player. - // This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead. + // This it intentionally does not operate on object count and uses only score instead. double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1); double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1; @@ -426,13 +426,12 @@ namespace osu.Game.Database // ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score. double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT); - double lowerEstimateOfComboPortionInStandardisedScore + double scoreBasedEstimateOfComboPortionInStandardisedScore = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore; // Compute approximate upper estimate new score for that play. // This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths. - // There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice. remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1; double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss); // Because we assumed all combos were equal, `remainingComboPortionInScoreV1` @@ -449,7 +448,17 @@ namespace osu.Game.Database // we can skip adding the 1 and just multiply by x ^ 0.5. remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT); - double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore; + double objectCountBasedEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore; + + // Enforce some invariants on both of the estimates. + // In rare cases they can produce invalid results. + scoreBasedEstimateOfComboPortionInStandardisedScore = + Math.Clamp(scoreBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore); + objectCountBasedEstimateOfComboPortionInStandardisedScore = + Math.Clamp(objectCountBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore); + + double lowerEstimateOfComboPortionInStandardisedScore = Math.Min(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore); + double upperEstimateOfComboPortionInStandardisedScore = Math.Max(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore); // Approximate by combining lower and upper estimates. // As the lower-estimate is very pessimistic, we use a 30/70 ratio From 5b6703ec0da46259daddd777ee88be5317aba2d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Mar 2024 19:23:29 +0800 Subject: [PATCH 79/88] Move optimisation to isolated method --- osu.Game/Storyboards/StoryboardSprite.cs | 76 +++++++++++++----------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 350438942e..4992ae128d 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -90,41 +90,8 @@ namespace osu.Game.Storyboards // Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex. if (loops.Count == 0) { - // Here we are starting from maximum value and trying to minimise the end time on each step. - // There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0. - double[] deathTimes = - { - double.MaxValue, // alpha - double.MaxValue, // colour alpha - double.MaxValue, // scale - double.MaxValue, // scale x - double.MaxValue, // scale y - }; - - // The loops below are following the same pattern. - // We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row - // so we are saving the earliest of them. - foreach (var alphaCommand in TimelineGroup.Alpha.Commands) - { - deathTimes[0] = alphaCommand.EndValue == 0 - ? Math.Min(alphaCommand.EndTime, deathTimes[0]) // commands are ordered by the start time, however end time may vary. Save the earliest. - : double.MaxValue; // If value isn't 0 (sprite becomes visible again), revert the saved state. - } - - foreach (var colourCommand in TimelineGroup.Colour.Commands) - deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue; - - foreach (var scaleCommand in TimelineGroup.Scale.Commands) - deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue; - - foreach (var scaleCommand in TimelineGroup.VectorScale.Commands) - { - deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue; - deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue; - } - // Take the minimum time of all the potential "death" reasons. - latestEndTime = deathTimes.Min(); + latestEndTime = calculateOptimisedEndTime(TimelineGroup); } // If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue @@ -238,6 +205,47 @@ namespace osu.Game.Storyboards return commands; } + private static double calculateOptimisedEndTime(CommandTimelineGroup timelineGroup) + { + // Here we are starting from maximum value and trying to minimise the end time on each step. + // There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0. + double[] deathTimes = + { + double.MaxValue, // alpha + double.MaxValue, // colour alpha + double.MaxValue, // scale + double.MaxValue, // scale x + double.MaxValue, // scale y + }; + + // The loops below are following the same pattern. + // We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row + // so we are saving the earliest of them. + foreach (var alphaCommand in timelineGroup.Alpha.Commands) + { + if (alphaCommand.EndValue == 0) + // commands are ordered by the start time, however end time may vary. Save the earliest. + deathTimes[0] = Math.Min(alphaCommand.EndTime, deathTimes[0]); + else + // If value isn't 0 (sprite becomes visible again), revert the saved state. + deathTimes[0] = double.MaxValue; + } + + foreach (var colourCommand in timelineGroup.Colour.Commands) + deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue; + + foreach (var scaleCommand in timelineGroup.Scale.Commands) + deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue; + + foreach (var scaleCommand in timelineGroup.VectorScale.Commands) + { + deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue; + deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue; + } + + return deathTimes.Min(); + } + public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; From 672f645cbab747d02cbb61d31283f604a3244ed8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 6 Mar 2024 18:39:18 +0300 Subject: [PATCH 80/88] Clamp only on horizontal sides --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs index 6ba91fbbd5..8f9a2d7e74 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs @@ -47,19 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy AutoSizeAxes = Axes.Y, Children = new Drawable[] { + // Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key. upSprite = new Sprite { Origin = Anchor.BottomCentre, - // ClampToEdge is used to avoid gaps between keys, see: https://github.com/ppy/osu/issues/27431 - Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge), + Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default), RelativeSizeAxes = Axes.X, Width = 1 }, downSprite = new Sprite { Origin = Anchor.BottomCentre, - // ClampToEdge is used to avoid gaps between keys, see: https://github.com/ppy/osu/issues/27431 - Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge), + Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default), RelativeSizeAxes = Axes.X, Width = 1, Alpha = 0 From 3121cf81e6f8ad0dd8ecb80b5dfc2cff4f55c306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Mar 2024 21:30:09 +0100 Subject: [PATCH 81/88] Bump score version --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 775d87f3f2..4ee4231925 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -45,9 +45,10 @@ namespace osu.Game.Scoring.Legacy /// /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// 30000014: Fix edge cases in conversion for osu! scores on selected beatmaps. Reconvert all scores. + /// 30000015: Fix osu! standardised score estimation algorithm violating basic invariants. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000014; + public const int LATEST_VERSION = 30000015; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From aa3cd402ca9ed3d666aac0e9934f9290af01b828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Mar 2024 21:30:31 +0100 Subject: [PATCH 82/88] Fix broken english --- 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 53ff1a25ca..6f2f8d64fa 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -415,7 +415,7 @@ namespace osu.Game.Database // Calculate how many times the longest combo the user has achieved in the play can repeat // without exceeding the combo portion in score V1 as achieved by the player. - // This it intentionally does not operate on object count and uses only score instead. + // This intentionally does not operate on object count and uses only score instead. double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1); double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1; From 336a6180e537ba1c205c488cead7c5494446b705 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 7 Mar 2024 08:20:20 +0300 Subject: [PATCH 83/88] Expose `TRANSITION_LENGTH` from tab control --- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index c260c92b43..f24977927f 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -116,18 +116,18 @@ namespace osu.Game.Graphics.UserInterface } } - private const float transition_length = 500; + protected const float TRANSITION_LENGTH = 500; - protected void FadeHovered() + protected virtual void FadeHovered() { - Bar.FadeIn(transition_length, Easing.OutQuint); - Text.FadeColour(Color4.White, transition_length, Easing.OutQuint); + Bar.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + Text.FadeColour(Color4.White, TRANSITION_LENGTH, Easing.OutQuint); } - protected void FadeUnhovered() + protected virtual void FadeUnhovered() { - Bar.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint); - Text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint); + Bar.FadeTo(IsHovered ? 1 : 0, TRANSITION_LENGTH, Easing.OutQuint); + Text.FadeColour(IsHovered ? Color4.White : AccentColour, TRANSITION_LENGTH, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) From 0fe139a1892423030b1de36df957bff095fc6587 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 7 Mar 2024 08:20:46 +0300 Subject: [PATCH 84/88] Adjust editor screen switcher control design and behaviour --- .../Menus/EditorScreenSwitcherControl.cs | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs b/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs index 1f6d61d0ad..2b0b1ea219 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs @@ -9,6 +9,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Components.Menus { @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Edit.Components.Menus TabContainer.RelativeSizeAxes &= ~Axes.X; TabContainer.AutoSizeAxes = Axes.X; - TabContainer.Padding = new MarginPadding(10); + TabContainer.Spacing = Vector2.Zero; } [BackgroundDependencyLoader] @@ -42,30 +43,51 @@ namespace osu.Game.Screens.Edit.Components.Menus private partial class TabItem : OsuTabItem { - private const float transition_length = 250; + private readonly Box background; + private Color4 backgroundIdleColour; + private Color4 backgroundHoverColour; public TabItem(EditorScreenMode value) : base(value) { - Text.Margin = new MarginPadding(); + Text.Margin = new MarginPadding(10); Text.Anchor = Anchor.CentreLeft; Text.Origin = Anchor.CentreLeft; Text.Font = OsuFont.TorusAlternate; + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }); + Bar.Expire(); } - protected override void OnActivated() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - base.OnActivated(); - Bar.ScaleTo(new Vector2(1, 5), transition_length, Easing.OutQuint); + backgroundIdleColour = colourProvider.Background2; + backgroundHoverColour = colourProvider.Background1; } - protected override void OnDeactivated() + protected override void LoadComplete() { - base.OnDeactivated(); - Bar.ScaleTo(Vector2.One, transition_length, Easing.OutQuint); + base.LoadComplete(); + background.Colour = backgroundIdleColour; + } + + protected override void FadeHovered() + { + base.FadeHovered(); + background.FadeColour(backgroundHoverColour, TRANSITION_LENGTH, Easing.OutQuint); + } + + protected override void FadeUnhovered() + { + base.FadeUnhovered(); + background.FadeColour(backgroundIdleColour, TRANSITION_LENGTH, Easing.OutQuint); } } } From 56caf1935043112ec13c5d0b1d43e9d23b8d1f7e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 6 Mar 2024 22:48:54 -0800 Subject: [PATCH 85/88] Add visual test for failed S display --- .../TestSceneExpandedPanelMiddleContent.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index d71c72f4ec..ceb2d4927c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -88,8 +88,21 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("play time not displayed", () => !this.ChildrenOfType().Any()); } - private void showPanel(ScoreInfo score) => - Child = new ExpandedPanelMiddleContentContainer(score); + [TestCase(false)] + [TestCase(true)] + public void TestFailedSDisplay(bool withFlair) + { + AddStep("show failed S score", () => + { + var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser())); + score.Rank = ScoreRank.A; + score.Accuracy = 0.975; + showPanel(score, withFlair); + }); + } + + private void showPanel(ScoreInfo score, bool withFlair = false) => + Child = new ExpandedPanelMiddleContentContainer(score, withFlair); private BeatmapInfo createTestBeatmap([NotNull] RealmUser author) { @@ -107,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking private partial class ExpandedPanelMiddleContentContainer : Container { - public ExpandedPanelMiddleContentContainer(ScoreInfo score) + public ExpandedPanelMiddleContentContainer(ScoreInfo score, bool withFlair) { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -119,7 +132,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#444"), }, - new ExpandedPanelMiddleContent(score) + new ExpandedPanelMiddleContent(score, withFlair) }; } } From c36232bc02ff9289b0ecc16a6235049f3ddaa63a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 6 Mar 2024 20:08:46 -0800 Subject: [PATCH 86/88] Fix results screen accuracy circle not showing correctly for failed S with no flair --- .../Expanded/Accuracy/AccuracyCircle.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 54829a6274..f04e4a6444 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -194,11 +194,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy rankText = new RankText(score.Rank) }; + if (isFailedSDueToMisses) + AddInternal(failedSRankText = new RankText(ScoreRank.S)); + if (withFlair) { - if (isFailedSDueToMisses) - AddInternal(failedSRankText = new RankText(ScoreRank.S)); - var applauseSamples = new List { applauseSampleName }; if (score.Rank >= ScoreRank.B) // when rank is B or higher, play legacy applause sample on legacy skins. @@ -326,24 +326,25 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { rankText.Appear(); - if (!withFlair) return; - - Schedule(() => - { - isTicking = false; - rankImpactSound.Play(); - }); - - const double applause_pre_delay = 545f; - const double applause_volume = 0.8f; - - using (BeginDelayedSequence(applause_pre_delay)) + if (withFlair) { Schedule(() => { - rankApplauseSound.VolumeTo(applause_volume); - rankApplauseSound.Play(); + isTicking = false; + rankImpactSound.Play(); }); + + const double applause_pre_delay = 545f; + const double applause_volume = 0.8f; + + using (BeginDelayedSequence(applause_pre_delay)) + { + Schedule(() => + { + rankApplauseSound.VolumeTo(applause_volume); + rankApplauseSound.Play(); + }); + } } } From 039520d55dc9b9cc55e143a6da8bb87f684ef882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 7 Mar 2024 09:49:20 +0100 Subject: [PATCH 87/88] Use slightly nicer parameterisation in test --- .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index ceb2d4927c..d97946a1d5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -88,9 +88,8 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("play time not displayed", () => !this.ChildrenOfType().Any()); } - [TestCase(false)] - [TestCase(true)] - public void TestFailedSDisplay(bool withFlair) + [Test] + public void TestFailedSDisplay([Values] bool withFlair) { AddStep("show failed S score", () => { From ca92a31cf97bcb28fd8d9e67fe0ac83b1085ac80 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Mar 2024 21:10:11 +0900 Subject: [PATCH 88/88] Fix missing event unbinds --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 9 +++++++++ .../Components/Timeline/TimelineHitObjectBlueprint.cs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 5ddc627642..3365b206cf 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mods; @@ -101,6 +102,14 @@ namespace osu.Game.Rulesets.Mania.Mods return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin.IsNotNull()) + skin.SourceChanged -= onSkinChanged; + } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 47dc3fb82e..d4afcc0151 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -265,6 +266,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return !Precision.AlmostIntersects(maskingBounds, rect); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin.IsNotNull()) + skin.SourceChanged -= updateColour; + } + private partial class Tick : Circle { public Tick()